From c1e86375167273422b94938337b93ff3a1233b9c Mon Sep 17 00:00:00 2001 From: BlubbFish Date: Tue, 3 Dec 2019 18:44:25 +0100 Subject: [PATCH] Coding Styles --- Unosquare.Swan/Abstractions/AppWorkerBase.cs | 411 ++-- .../AppWorkerStateChangedEventArgs.cs | 69 +- Unosquare.Swan/Components/CircularBuffer.cs | 392 ++- Unosquare.Swan/Components/CsProjFile.cs | 204 +- .../Components/CsProjMetadataBase.cs | 25 +- Unosquare.Swan/Components/DelayProvider.cs | 273 ++- .../Components/DependencyContainer.cs | 1393 +++++------ .../DependencyContainerResolveOptions.cs | 209 +- .../Components/IMessageHubMessage.cs | 22 +- Unosquare.Swan/Components/MessageHub.cs | 881 ++++--- .../Components/MessageHubMessageBase.cs | 104 +- .../Components/MessageHubSubscriptionToken.cs | 91 +- .../Components/ObjectFactoryBase.cs | 784 +++--- Unosquare.Swan/Components/ProcessResult.cs | 91 +- Unosquare.Swan/Components/ProcessRunner.cs | 911 ++++--- Unosquare.Swan/Components/RealtimeClock.cs | 265 +-- Unosquare.Swan/Components/RegisterOptions.cs | 242 +- Unosquare.Swan/Components/TypeRegistration.cs | 128 +- .../Components/TypesConcurrentDictionary.cs | 654 +++-- Unosquare.Swan/Enums.cs | 48 +- Unosquare.Swan/Eventing.ConnectionListener.cs | 293 ++- Unosquare.Swan/Eventing.cs | 171 +- ...ependencyContainerRegistrationException.cs | 79 +- .../DependencyContainerResolutionException.cs | 54 +- ...pendencyContainerWeakReferenceException.cs | 35 +- .../Exceptions/DnsQueryException.cs | 69 +- .../Exceptions/JsonRequestException.cs | 93 +- Unosquare.Swan/Exceptions/LdapException.cs | 261 +- Unosquare.Swan/Extensions.MimeMessage.cs | 96 +- Unosquare.Swan/Extensions.Network.cs | 110 +- Unosquare.Swan/Extensions.WindowsServices.cs | 139 +- Unosquare.Swan/Formatters/BitmapBuffer.cs | 353 +-- Unosquare.Swan/Models/OkOrError.cs | 94 +- Unosquare.Swan/Network.cs | 854 ++++--- Unosquare.Swan/Networking/Connection.cs | 1732 +++++++------- .../Networking/ConnectionListener.cs | 479 ++-- .../Networking/DnsClient.Interfaces.cs | 154 +- .../Networking/DnsClient.Request.cs | 1247 +++++----- .../Networking/DnsClient.ResourceRecords.cs | 890 ++++--- .../Networking/DnsClient.Response.cs | 418 ++-- Unosquare.Swan/Networking/DnsClient.cs | 163 +- Unosquare.Swan/Networking/DnsQueryResult.cs | 249 +- Unosquare.Swan/Networking/DnsRecord.cs | 442 ++-- Unosquare.Swan/Networking/Enums.Dns.cs | 337 ++- Unosquare.Swan/Networking/Enums.Smtp.cs | 314 ++- Unosquare.Swan/Networking/JsonClient.cs | 764 +++--- Unosquare.Swan/Networking/Ldap/Asn1.cs | 1157 +++++---- Unosquare.Swan/Networking/Ldap/LberDecoder.cs | 325 ++- Unosquare.Swan/Networking/Ldap/LberEncoder.cs | 463 ++-- .../Networking/Ldap/LdapConnection.cs | 774 +++--- Unosquare.Swan/Networking/Ldap/LdapControl.cs | 547 +++-- Unosquare.Swan/Networking/Ldap/LdapEntry.cs | 1514 ++++++------ Unosquare.Swan/Networking/Ldap/LdapEnums.cs | 247 +- Unosquare.Swan/Networking/Ldap/LdapMessage.cs | 254 +- .../Networking/Ldap/LdapModification.cs | 151 +- .../Networking/Ldap/LdapModifyRequest.cs | 122 +- .../Networking/Ldap/LdapOperation.cs | 221 +- .../Networking/Ldap/LdapSearchRequest.cs | 359 ++- .../Networking/Ldap/LdapSearchResults.cs | 176 +- .../Networking/Ldap/LdapStatusCode.cs | 1070 +++++---- Unosquare.Swan/Networking/Ldap/Message.cs | 586 +++-- Unosquare.Swan/Networking/Ldap/RfcControl.cs | 243 +- Unosquare.Swan/Networking/Ldap/RfcFilter.cs | 2114 ++++++++--------- Unosquare.Swan/Networking/Ldap/RfcLdap.cs | 470 ++-- .../Networking/Ldap/RfcLdapMessage.cs | 737 +++--- .../Networking/Ldap/RfcModifyRequest.cs | 86 +- Unosquare.Swan/Networking/SmtpClient.cs | 769 +++--- Unosquare.Swan/Networking/SmtpDefinitions.cs | 51 +- Unosquare.Swan/Networking/SmtpSender.cs | 107 +- Unosquare.Swan/Networking/SmtpServerReply.cs | 504 ++-- Unosquare.Swan/Networking/SmtpSessionState.cs | 339 +-- Unosquare.Swan/Networking/SnmpClient.cs | 483 ++-- 72 files changed, 15024 insertions(+), 15932 deletions(-) diff --git a/Unosquare.Swan/Abstractions/AppWorkerBase.cs b/Unosquare.Swan/Abstractions/AppWorkerBase.cs index 583b626..a8239a9 100644 --- a/Unosquare.Swan/Abstractions/AppWorkerBase.cs +++ b/Unosquare.Swan/Abstractions/AppWorkerBase.cs @@ -1,214 +1,203 @@ -namespace Unosquare.Swan.Abstractions -{ - using System; - using System.Threading; - using System.Threading.Tasks; - +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Unosquare.Swan.Abstractions { + /// + /// A base implementation of an Application service containing a worker task that performs background processing. + /// + /// + /// The following code describes how to implement the class. + /// + /// using System; + /// using System.Threading.Tasks; + /// using Unosquare.Swan; + /// using Unosquare.Swan.Abstractions; + /// + /// class Worker : AppWorkerBase + /// { + /// // an action that will be executed if the worker is stopped + /// public Action OnExit { get; set; } + /// + /// // override the base loop method, this is the code will + /// // execute until the cancellation token is canceled. + /// protected override Task WorkerThreadLoop() + /// { + /// // delay a second and then proceed + /// await Task.Delay(TimeSpan.FromMilliseconds(1000), CancellationToken); + /// + /// // just print out this + /// $"Working...".WriteLine(); + /// } + /// + /// // Once the worker is stopped this code will be executed + /// protected override void OnWorkerThreadExit() + /// { + /// // execute the base method + /// base.OnWorkerThreadExit(); + /// + /// // then if the OnExit Action is not null execute it + /// OnExit?.Invoke(); + /// } + /// } + /// + /// + public abstract class AppWorkerBase + : IWorker, IDisposable { + private readonly Object _syncLock = new Object(); + private AppWorkerState _workerState = AppWorkerState.Stopped; + private CancellationTokenSource _tokenSource; + /// - /// A base implementation of an Application service containing a worker task that performs background processing. + /// Initializes a new instance of the class. /// - /// - /// The following code describes how to implement the class. - /// - /// using System; - /// using System.Threading.Tasks; - /// using Unosquare.Swan; - /// using Unosquare.Swan.Abstractions; - /// - /// class Worker : AppWorkerBase - /// { - /// // an action that will be executed if the worker is stopped - /// public Action OnExit { get; set; } - /// - /// // override the base loop method, this is the code will - /// // execute until the cancellation token is canceled. - /// protected override Task WorkerThreadLoop() - /// { - /// // delay a second and then proceed - /// await Task.Delay(TimeSpan.FromMilliseconds(1000), CancellationToken); - /// - /// // just print out this - /// $"Working...".WriteLine(); - /// } - /// - /// // Once the worker is stopped this code will be executed - /// protected override void OnWorkerThreadExit() - /// { - /// // execute the base method - /// base.OnWorkerThreadExit(); - /// - /// // then if the OnExit Action is not null execute it - /// OnExit?.Invoke(); - /// } - /// } - /// - /// - public abstract class AppWorkerBase - : IWorker, IDisposable - { - private readonly object _syncLock = new object(); - private AppWorkerState _workerState = AppWorkerState.Stopped; - private CancellationTokenSource _tokenSource; - - /// - /// Initializes a new instance of the class. - /// - protected AppWorkerBase() - { - State = AppWorkerState.Stopped; - IsBusy = false; - } - - /// - /// Occurs when [state changed]. - /// - public event EventHandler StateChanged; - - #region Properties - - /// - /// Gets the state of the application service. - /// In other words, useful to know whether the service is running. - /// - /// - /// The state. - /// - public AppWorkerState State - { - get => _workerState; - - private set - { - lock (_syncLock) - { - if (value == _workerState) return; - - $"Service state changing from {State} to {value}".Debug(GetType().Name); - var newState = value; - var oldState = _workerState; - _workerState = value; - - StateChanged?.Invoke(this, new AppWorkerStateChangedEventArgs(oldState, newState)); - } - } - } - - /// - /// Gets the cancellation token. - /// - /// - /// The cancellation token. - /// - public CancellationToken CancellationToken => _tokenSource?.Token ?? default; - - /// - /// Gets a value indicating whether the thread is busy. - /// - /// - /// true if this instance is busy; otherwise, false. - /// - public bool IsBusy { get; private set; } - - #endregion - - #region AppWorkerBase Methods - - /// - /// Performs internal service initialization tasks required before starting the service. - /// - /// Service cannot be initialized because it seems to be currently running. - public virtual void Initialize() => CheckIsRunning(); - - /// - /// Service cannot be started because it seems to be currently running. - public virtual void Start() - { - CheckIsRunning(); - - CreateWorker(); - State = AppWorkerState.Running; - } - - /// - /// Service cannot be stopped because it is not running. - public virtual void Stop() - { - if (State != AppWorkerState.Running) - return; - - _tokenSource?.Cancel(); - "Service stop requested.".Debug(GetType().Name); - State = AppWorkerState.Stopped; - } - - /// - public void Dispose() => _tokenSource?.Dispose(); - - #endregion - - #region Abstract and Virtual Methods - - /// - /// Called when an unhandled exception is thrown. - /// - /// The ex. - protected virtual void OnWorkerThreadLoopException(Exception ex) - => "Service exception detected.".Debug(GetType().Name, ex); - - /// - /// This method is called when the user loop has exited. - /// - protected virtual void OnWorkerThreadExit() => "Service thread is stopping.".Debug(GetType().Name); - - /// - /// Implement this method as a loop that checks whether CancellationPending has been set to true - /// If so, immediately exit the loop. - /// - /// A task representing the execution of the worker. - protected abstract Task WorkerThreadLoop(); - - private void CheckIsRunning() - { - if (State != AppWorkerState.Stopped) - throw new InvalidOperationException("Service cannot be initialized because it seems to be currently running."); - } - - private void CreateWorker() - { - _tokenSource = new CancellationTokenSource(); - _tokenSource.Token.Register(() => - { - IsBusy = false; - OnWorkerThreadExit(); - }); - - Task.Run(async () => - { - IsBusy = true; - - try - { - while (!CancellationToken.IsCancellationRequested) - { - await WorkerThreadLoop().ConfigureAwait(false); - } - } - catch (AggregateException) - { - // Ignored - } - catch (Exception ex) - { - ex.Log(GetType().Name); - OnWorkerThreadLoopException(ex); - - if (!_tokenSource.IsCancellationRequested) - _tokenSource.Cancel(); - } - }, - _tokenSource.Token); - } - - #endregion - } + protected AppWorkerBase() { + this.State = AppWorkerState.Stopped; + this.IsBusy = false; + } + + /// + /// Occurs when [state changed]. + /// + public event EventHandler StateChanged; + + #region Properties + + /// + /// Gets the state of the application service. + /// In other words, useful to know whether the service is running. + /// + /// + /// The state. + /// + public AppWorkerState State { + get => this._workerState; + + private set { + lock(this._syncLock) { + if(value == this._workerState) { + return; + } + + $"Service state changing from {this.State} to {value}".Debug(this.GetType().Name); + AppWorkerState newState = value; + AppWorkerState oldState = this._workerState; + this._workerState = value; + + StateChanged?.Invoke(this, new AppWorkerStateChangedEventArgs(oldState, newState)); + } + } + } + + /// + /// Gets the cancellation token. + /// + /// + /// The cancellation token. + /// + public CancellationToken CancellationToken => this._tokenSource?.Token ?? default; + + /// + /// Gets a value indicating whether the thread is busy. + /// + /// + /// true if this instance is busy; otherwise, false. + /// + public Boolean IsBusy { + get; private set; + } + + #endregion + + #region AppWorkerBase Methods + + /// + /// Performs internal service initialization tasks required before starting the service. + /// + /// Service cannot be initialized because it seems to be currently running. + public virtual void Initialize() => this.CheckIsRunning(); + + /// + /// Service cannot be started because it seems to be currently running. + public virtual void Start() { + this.CheckIsRunning(); + + this.CreateWorker(); + this.State = AppWorkerState.Running; + } + + /// + /// Service cannot be stopped because it is not running. + public virtual void Stop() { + if(this.State != AppWorkerState.Running) { + return; + } + + this._tokenSource?.Cancel(); + "Service stop requested.".Debug(this.GetType().Name); + this.State = AppWorkerState.Stopped; + } + + /// + public void Dispose() => this._tokenSource?.Dispose(); + + #endregion + + #region Abstract and Virtual Methods + + /// + /// Called when an unhandled exception is thrown. + /// + /// The ex. + protected virtual void OnWorkerThreadLoopException(Exception ex) + => "Service exception detected.".Debug(this.GetType().Name, ex); + + /// + /// This method is called when the user loop has exited. + /// + protected virtual void OnWorkerThreadExit() => "Service thread is stopping.".Debug(this.GetType().Name); + + /// + /// Implement this method as a loop that checks whether CancellationPending has been set to true + /// If so, immediately exit the loop. + /// + /// A task representing the execution of the worker. + protected abstract Task WorkerThreadLoop(); + + private void CheckIsRunning() { + if(this.State != AppWorkerState.Stopped) { + throw new InvalidOperationException("Service cannot be initialized because it seems to be currently running."); + } + } + + private void CreateWorker() { + this._tokenSource = new CancellationTokenSource(); + _ = this._tokenSource.Token.Register(() => { + this.IsBusy = false; + this.OnWorkerThreadExit(); + }); + + _ = Task.Run(async () => { + this.IsBusy = true; + + try { + while(!this.CancellationToken.IsCancellationRequested) { + await this.WorkerThreadLoop().ConfigureAwait(false); + } + } catch(AggregateException) { + // Ignored + } catch(Exception ex) { + ex.Log(this.GetType().Name); + this.OnWorkerThreadLoopException(ex); + + if(!this._tokenSource.IsCancellationRequested) { + this._tokenSource.Cancel(); + } + } + }, + this._tokenSource.Token); + } + + #endregion + } } \ No newline at end of file diff --git a/Unosquare.Swan/Abstractions/AppWorkerStateChangedEventArgs.cs b/Unosquare.Swan/Abstractions/AppWorkerStateChangedEventArgs.cs index a34e6fe..485475e 100644 --- a/Unosquare.Swan/Abstractions/AppWorkerStateChangedEventArgs.cs +++ b/Unosquare.Swan/Abstractions/AppWorkerStateChangedEventArgs.cs @@ -1,37 +1,38 @@ -namespace Unosquare.Swan.Abstractions -{ - using System; - +using System; + +namespace Unosquare.Swan.Abstractions { + /// + /// Represents event arguments whenever the state of an application service changes. + /// + public class AppWorkerStateChangedEventArgs : EventArgs { /// - /// Represents event arguments whenever the state of an application service changes. + /// Initializes a new instance of the class. /// - public class AppWorkerStateChangedEventArgs : EventArgs - { - /// - /// Initializes a new instance of the class. - /// - /// The old state. - /// The new state. - public AppWorkerStateChangedEventArgs(AppWorkerState oldState, AppWorkerState newState) - { - OldState = oldState; - NewState = newState; - } - - /// - /// Gets the state to which the application service changed. - /// - /// - /// The new state. - /// - public AppWorkerState NewState { get; } - - /// - /// Gets the old state. - /// - /// - /// The old state. - /// - public AppWorkerState OldState { get; } - } + /// The old state. + /// The new state. + public AppWorkerStateChangedEventArgs(AppWorkerState oldState, AppWorkerState newState) { + this.OldState = oldState; + this.NewState = newState; + } + + /// + /// Gets the state to which the application service changed. + /// + /// + /// The new state. + /// + public AppWorkerState NewState { + get; + } + + /// + /// Gets the old state. + /// + /// + /// The old state. + /// + public AppWorkerState OldState { + get; + } + } } diff --git a/Unosquare.Swan/Components/CircularBuffer.cs b/Unosquare.Swan/Components/CircularBuffer.cs index c8cf6db..3aed66c 100644 --- a/Unosquare.Swan/Components/CircularBuffer.cs +++ b/Unosquare.Swan/Components/CircularBuffer.cs @@ -1,204 +1,202 @@ -namespace Unosquare.Swan.Components -{ - using System; - using System.Runtime.InteropServices; - +using System; +using System.Runtime.InteropServices; + +namespace Unosquare.Swan.Components { + /// + /// A fixed-size buffer that acts as an infinite length one. + /// This buffer is backed by unmanaged, very fast memory so ensure you call + /// the dispose method when you are done using it. + /// Only for Windows. + /// + /// + public sealed class CircularBuffer : IDisposable { + private readonly Object _syncLock = new Object(); + private IntPtr _buffer = IntPtr.Zero; + /// - /// A fixed-size buffer that acts as an infinite length one. - /// This buffer is backed by unmanaged, very fast memory so ensure you call - /// the dispose method when you are done using it. - /// Only for Windows. + /// Initializes a new instance of the class. /// - /// - public sealed class CircularBuffer : IDisposable - { - private readonly object _syncLock = new object(); - private IntPtr _buffer = IntPtr.Zero; - - /// - /// Initializes a new instance of the class. - /// - /// Length of the buffer. - public CircularBuffer(int bufferLength) - { + /// Length of the buffer. + public CircularBuffer(Int32 bufferLength) { #if !NET452 if (Runtime.OS != Swan.OperatingSystem.Windows) throw new InvalidOperationException("CircularBuffer component is only available in Windows"); #endif - - Length = bufferLength; - _buffer = Marshal.AllocHGlobal(Length); - } - - #region Properties - - /// - /// Gets the capacity of this buffer. - /// - /// - /// The length. - /// - public int Length { get; private set; } - - /// - /// Gets the current, 0-based read index. - /// - /// - /// The index of the read. - /// - public int ReadIndex { get; private set; } - - /// - /// Gets the current, 0-based write index. - /// - /// - /// The index of the write. - /// - public int WriteIndex { get; private set; } - - /// - /// Gets an the object associated with the last write. - /// - /// - /// The write tag. - /// - public TimeSpan WriteTag { get; private set; } = TimeSpan.MinValue; - - /// - /// Gets the available bytes to read. - /// - /// - /// The readable count. - /// - public int ReadableCount { get; private set; } - - /// - /// Gets the number of bytes that can be written. - /// - /// - /// The writable count. - /// - public int WritableCount => Length - ReadableCount; - - /// - /// Gets percentage of used bytes (readbale/available, from 0.0 to 1.0). - /// - /// - /// The capacity percent. - /// - public double CapacityPercent => 1.0 * ReadableCount / Length; - - #endregion - - #region Methods - - /// - /// Reads the specified number of bytes into the target array. - /// - /// The requested bytes. - /// The target. - /// The target offset. - /// - /// Exception that is thrown when a method call is invalid for the object's current state. - /// - public void Read(int requestedBytes, byte[] target, int targetOffset) - { - lock (_syncLock) - { - if (requestedBytes > ReadableCount) - { - throw new InvalidOperationException( - $"Unable to read {requestedBytes} bytes. Only {ReadableCount} bytes are available"); - } - - var readCount = 0; - while (readCount < requestedBytes) - { - var copyLength = Math.Min(Length - ReadIndex, requestedBytes - readCount); - var sourcePtr = _buffer + ReadIndex; - Marshal.Copy(sourcePtr, target, targetOffset + readCount, copyLength); - - readCount += copyLength; - ReadIndex += copyLength; - ReadableCount -= copyLength; - - if (ReadIndex >= Length) - ReadIndex = 0; - } - } - } - - /// - /// Writes data to the backing buffer using the specified pointer and length. - /// and associating a write tag for this operation. - /// - /// The source. - /// The length. - /// The write tag. - /// Unable to write to circular buffer. Call the Read method to make some additional room. - public void Write(IntPtr source, int length, TimeSpan writeTag) - { - lock (_syncLock) - { - if (ReadableCount + length > Length) - { - throw new InvalidOperationException( - $"Unable to write to circular buffer. Call the {nameof(Read)} method to make some additional room."); - } - - var writeCount = 0; - while (writeCount < length) - { - var copyLength = Math.Min(Length - WriteIndex, length - writeCount); - var sourcePtr = source + writeCount; - var targetPtr = _buffer + WriteIndex; - CopyMemory(targetPtr, sourcePtr, (uint) copyLength); - - writeCount += copyLength; - WriteIndex += copyLength; - ReadableCount += copyLength; - - if (WriteIndex >= Length) - WriteIndex = 0; - } - - WriteTag = writeTag; - } - } - - /// - /// Resets all states as if this buffer had just been created. - /// - public void Clear() - { - lock (_syncLock) - { - WriteIndex = 0; - ReadIndex = 0; - WriteTag = TimeSpan.MinValue; - ReadableCount = 0; - } - } - - /// - public void Dispose() - { - if (_buffer == IntPtr.Zero) return; - - Marshal.FreeHGlobal(_buffer); - _buffer = IntPtr.Zero; - Length = 0; - } - - /// - /// Fast pointer memory block copy function. - /// - /// The destination. - /// The source. - /// The length. - [DllImport("kernel32")] - public static extern void CopyMemory(IntPtr destination, IntPtr source, uint length); - - #endregion - } + + this.Length = bufferLength; + this._buffer = Marshal.AllocHGlobal(this.Length); + } + + #region Properties + + /// + /// Gets the capacity of this buffer. + /// + /// + /// The length. + /// + public Int32 Length { + get; private set; + } + + /// + /// Gets the current, 0-based read index. + /// + /// + /// The index of the read. + /// + public Int32 ReadIndex { + get; private set; + } + + /// + /// Gets the current, 0-based write index. + /// + /// + /// The index of the write. + /// + public Int32 WriteIndex { + get; private set; + } + + /// + /// Gets an the object associated with the last write. + /// + /// + /// The write tag. + /// + public TimeSpan WriteTag { get; private set; } = TimeSpan.MinValue; + + /// + /// Gets the available bytes to read. + /// + /// + /// The readable count. + /// + public Int32 ReadableCount { + get; private set; + } + + /// + /// Gets the number of bytes that can be written. + /// + /// + /// The writable count. + /// + public Int32 WritableCount => this.Length - this.ReadableCount; + + /// + /// Gets percentage of used bytes (readbale/available, from 0.0 to 1.0). + /// + /// + /// The capacity percent. + /// + public Double CapacityPercent => 1.0 * this.ReadableCount / this.Length; + + #endregion + + #region Methods + + /// + /// Reads the specified number of bytes into the target array. + /// + /// The requested bytes. + /// The target. + /// The target offset. + /// + /// Exception that is thrown when a method call is invalid for the object's current state. + /// + public void Read(Int32 requestedBytes, Byte[] target, Int32 targetOffset) { + lock(this._syncLock) { + if(requestedBytes > this.ReadableCount) { + throw new InvalidOperationException( + $"Unable to read {requestedBytes} bytes. Only {this.ReadableCount} bytes are available"); + } + + Int32 readCount = 0; + while(readCount < requestedBytes) { + Int32 copyLength = Math.Min(this.Length - this.ReadIndex, requestedBytes - readCount); + IntPtr sourcePtr = this._buffer + this.ReadIndex; + Marshal.Copy(sourcePtr, target, targetOffset + readCount, copyLength); + + readCount += copyLength; + this.ReadIndex += copyLength; + this.ReadableCount -= copyLength; + + if(this.ReadIndex >= this.Length) { + this.ReadIndex = 0; + } + } + } + } + + /// + /// Writes data to the backing buffer using the specified pointer and length. + /// and associating a write tag for this operation. + /// + /// The source. + /// The length. + /// The write tag. + /// Unable to write to circular buffer. Call the Read method to make some additional room. + public void Write(IntPtr source, Int32 length, TimeSpan writeTag) { + lock(this._syncLock) { + if(this.ReadableCount + length > this.Length) { + throw new InvalidOperationException( + $"Unable to write to circular buffer. Call the {nameof(Read)} method to make some additional room."); + } + + Int32 writeCount = 0; + while(writeCount < length) { + Int32 copyLength = Math.Min(this.Length - this.WriteIndex, length - writeCount); + IntPtr sourcePtr = source + writeCount; + IntPtr targetPtr = this._buffer + this.WriteIndex; + CopyMemory(targetPtr, sourcePtr, (UInt32)copyLength); + + writeCount += copyLength; + this.WriteIndex += copyLength; + this.ReadableCount += copyLength; + + if(this.WriteIndex >= this.Length) { + this.WriteIndex = 0; + } + } + + this.WriteTag = writeTag; + } + } + + /// + /// Resets all states as if this buffer had just been created. + /// + public void Clear() { + lock(this._syncLock) { + this.WriteIndex = 0; + this.ReadIndex = 0; + this.WriteTag = TimeSpan.MinValue; + this.ReadableCount = 0; + } + } + + /// + public void Dispose() { + if(this._buffer == IntPtr.Zero) { + return; + } + + Marshal.FreeHGlobal(this._buffer); + this._buffer = IntPtr.Zero; + this.Length = 0; + } + + /// + /// Fast pointer memory block copy function. + /// + /// The destination. + /// The source. + /// The length. + [DllImport("kernel32")] + public static extern void CopyMemory(IntPtr destination, IntPtr source, UInt32 length); + + #endregion + } } \ No newline at end of file diff --git a/Unosquare.Swan/Components/CsProjFile.cs b/Unosquare.Swan/Components/CsProjFile.cs index 12b05db..39358c5 100644 --- a/Unosquare.Swan/Components/CsProjFile.cs +++ b/Unosquare.Swan/Components/CsProjFile.cs @@ -1,109 +1,101 @@ -namespace Unosquare.Swan.Components -{ - using System; - using System.IO; - using System.Linq; - using System.Xml.Linq; - +using System; +using System.IO; +using System.Linq; +using System.Xml.Linq; + +namespace Unosquare.Swan.Components { + /// + /// Represents a CsProjFile (and FsProjFile) parser. + /// + /// + /// Based on https://github.com/maartenba/dotnetcli-init. + /// + /// The type of CsProjMetadataBase. + /// + public class CsProjFile + : IDisposable + where T : CsProjMetadataBase { + private readonly Stream _stream; + private readonly Boolean _leaveOpen; + private readonly XDocument _xmlDocument; + /// - /// Represents a CsProjFile (and FsProjFile) parser. + /// Initializes a new instance of the class. /// - /// - /// Based on https://github.com/maartenba/dotnetcli-init. - /// - /// The type of CsProjMetadataBase. - /// - public class CsProjFile - : IDisposable - where T : CsProjMetadataBase - { - private readonly Stream _stream; - private readonly bool _leaveOpen; - private readonly XDocument _xmlDocument; - - /// - /// Initializes a new instance of the class. - /// - /// The filename. - public CsProjFile(string filename = null) - : this(OpenFile(filename)) - { - // placeholder - } - - /// - /// Initializes a new instance of the class. - /// - /// The stream. - /// if set to true [leave open]. - /// Project file is not of the new .csproj type. - public CsProjFile(Stream stream, bool leaveOpen = false) - { - _stream = stream; - _leaveOpen = leaveOpen; - - _xmlDocument = XDocument.Load(stream); - - var projectElement = _xmlDocument.Descendants("Project").FirstOrDefault(); - var sdkAttribute = projectElement?.Attribute("Sdk"); - var sdk = sdkAttribute?.Value; - if (sdk != "Microsoft.NET.Sdk" && sdk != "Microsoft.NET.Sdk.Web") - { - throw new ArgumentException("Project file is not of the new .csproj type."); - } - - Metadata = Activator.CreateInstance(); - Metadata.SetData(_xmlDocument); - } - - /// - /// Gets the metadata. - /// - /// - /// The nu get metadata. - /// - public T Metadata { get; } - - /// - /// Saves this instance. - /// - public void Save() - { - _stream.SetLength(0); - _stream.Position = 0; - - _xmlDocument.Save(_stream); - } - - /// - public void Dispose() - { - if (!_leaveOpen) - { - _stream?.Dispose(); - } - } - - private static FileStream OpenFile(string filename) - { - if (filename == null) - { - filename = Directory - .EnumerateFiles(Directory.GetCurrentDirectory(), "*.csproj", SearchOption.TopDirectoryOnly) - .FirstOrDefault(); - } - - if (filename == null) - { - filename = Directory - .EnumerateFiles(Directory.GetCurrentDirectory(), "*.fsproj", SearchOption.TopDirectoryOnly) - .FirstOrDefault(); - } - - if (string.IsNullOrWhiteSpace(filename)) - throw new ArgumentNullException(nameof(filename)); - - return File.Open(filename, FileMode.OpenOrCreate, FileAccess.ReadWrite); - } - } + /// The filename. + public CsProjFile(String filename = null) + : this(OpenFile(filename)) { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// + /// The stream. + /// if set to true [leave open]. + /// Project file is not of the new .csproj type. + public CsProjFile(Stream stream, Boolean leaveOpen = false) { + this._stream = stream; + this._leaveOpen = leaveOpen; + + this._xmlDocument = XDocument.Load(stream); + + XElement projectElement = this._xmlDocument.Descendants("Project").FirstOrDefault(); + XAttribute sdkAttribute = projectElement?.Attribute("Sdk"); + String sdk = sdkAttribute?.Value; + if(sdk != "Microsoft.NET.Sdk" && sdk != "Microsoft.NET.Sdk.Web") { + throw new ArgumentException("Project file is not of the new .csproj type."); + } + + this.Metadata = Activator.CreateInstance(); + this.Metadata.SetData(this._xmlDocument); + } + + /// + /// Gets the metadata. + /// + /// + /// The nu get metadata. + /// + public T Metadata { + get; + } + + /// + /// Saves this instance. + /// + public void Save() { + this._stream.SetLength(0); + this._stream.Position = 0; + + this._xmlDocument.Save(this._stream); + } + + /// + public void Dispose() { + if(!this._leaveOpen) { + this._stream?.Dispose(); + } + } + + private static FileStream OpenFile(String filename) { + if(filename == null) { + filename = Directory + .EnumerateFiles(Directory.GetCurrentDirectory(), "*.csproj", SearchOption.TopDirectoryOnly) + .FirstOrDefault(); + } + + if(filename == null) { + filename = Directory + .EnumerateFiles(Directory.GetCurrentDirectory(), "*.fsproj", SearchOption.TopDirectoryOnly) + .FirstOrDefault(); + } + + if(String.IsNullOrWhiteSpace(filename)) { + throw new ArgumentNullException(nameof(filename)); + } + + return File.Open(filename, FileMode.OpenOrCreate, FileAccess.ReadWrite); + } + } } \ No newline at end of file diff --git a/Unosquare.Swan/Components/CsProjMetadataBase.cs b/Unosquare.Swan/Components/CsProjMetadataBase.cs index d4085c4..ee939d1 100644 --- a/Unosquare.Swan/Components/CsProjMetadataBase.cs +++ b/Unosquare.Swan/Components/CsProjMetadataBase.cs @@ -1,8 +1,9 @@ -namespace Unosquare.Swan.Components +using System; +using System.Linq; +using System.Xml.Linq; + +namespace Unosquare.Swan.Components { - using System.Linq; - using System.Xml.Linq; - /// /// Represents a CsProj metadata abstract class /// to use with CsProjFile parser. @@ -17,7 +18,7 @@ /// /// The package identifier. /// - public string PackageId => FindElement(nameof(PackageId))?.Value; + public String PackageId => this.FindElement(nameof(this.PackageId))?.Value; /// /// Gets the name of the assembly. @@ -25,7 +26,7 @@ /// /// The name of the assembly. /// - public string AssemblyName => FindElement(nameof(AssemblyName))?.Value; + public String AssemblyName => this.FindElement(nameof(this.AssemblyName))?.Value; /// /// Gets the target frameworks. @@ -33,7 +34,7 @@ /// /// The target frameworks. /// - public string TargetFrameworks => FindElement(nameof(TargetFrameworks))?.Value; + public String TargetFrameworks => this.FindElement(nameof(this.TargetFrameworks))?.Value; /// /// Gets the target framework. @@ -41,7 +42,7 @@ /// /// The target framework. /// - public string TargetFramework => FindElement(nameof(TargetFramework))?.Value; + public String TargetFramework => this.FindElement(nameof(this.TargetFramework))?.Value; /// /// Gets the version. @@ -49,25 +50,25 @@ /// /// The version. /// - public string Version => FindElement(nameof(Version))?.Value; + public String Version => this.FindElement(nameof(this.Version))?.Value; /// /// Parses the cs proj tags. /// /// The arguments. - public abstract void ParseCsProjTags(ref string[] args); + public abstract void ParseCsProjTags(ref String[] args); /// /// Sets the data. /// /// The XML document. - public void SetData(XDocument xmlDocument) => _xmlDocument = xmlDocument; + public void SetData(XDocument xmlDocument) => this._xmlDocument = xmlDocument; /// /// Finds the element. /// /// Name of the element. /// A XElement. - protected XElement FindElement(string elementName) => _xmlDocument.Descendants(elementName).FirstOrDefault(); + protected XElement FindElement(String elementName) => this._xmlDocument.Descendants(elementName).FirstOrDefault(); } } \ No newline at end of file diff --git a/Unosquare.Swan/Components/DelayProvider.cs b/Unosquare.Swan/Components/DelayProvider.cs index 51a3a24..a1cc03c 100644 --- a/Unosquare.Swan/Components/DelayProvider.cs +++ b/Unosquare.Swan/Components/DelayProvider.cs @@ -1,144 +1,139 @@ -namespace Unosquare.Swan.Components -{ - using System; - using System.Diagnostics; - using System.Threading; - using System.Threading.Tasks; - using Abstractions; - +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Unosquare.Swan.Abstractions; + +namespace Unosquare.Swan.Components { + /// + /// Represents logic providing several delay mechanisms. + /// + /// + /// The following example shows how to implement delay mechanisms. + /// + /// using Unosquare.Swan.Components; + /// + /// public class Example + /// { + /// public static void Main() + /// { + /// // using the ThreadSleep strategy + /// using (var delay = new DelayProvider(DelayProvider.DelayStrategy.ThreadSleep)) + /// { + /// // retrieve how much time was delayed + /// var time = delay.WaitOne(); + /// } + /// } + /// } + /// + /// + public sealed class DelayProvider : IDisposable { + private readonly Object _syncRoot = new Object(); + private readonly Stopwatch _delayStopwatch = new Stopwatch(); + + private Boolean _isDisposed; + private IWaitEvent _delayEvent; + /// - /// Represents logic providing several delay mechanisms. + /// Initializes a new instance of the class. /// - /// - /// The following example shows how to implement delay mechanisms. - /// - /// using Unosquare.Swan.Components; - /// - /// public class Example - /// { - /// public static void Main() - /// { - /// // using the ThreadSleep strategy - /// using (var delay = new DelayProvider(DelayProvider.DelayStrategy.ThreadSleep)) - /// { - /// // retrieve how much time was delayed - /// var time = delay.WaitOne(); - /// } - /// } - /// } - /// - /// - public sealed class DelayProvider : IDisposable - { - private readonly object _syncRoot = new object(); - private readonly Stopwatch _delayStopwatch = new Stopwatch(); - - private bool _isDisposed; - private IWaitEvent _delayEvent; - - /// - /// Initializes a new instance of the class. - /// - /// The strategy. - public DelayProvider(DelayStrategy strategy = DelayStrategy.TaskDelay) - { - Strategy = strategy; - } - - /// - /// Enumerates the different ways of providing delays. - /// - public enum DelayStrategy - { - /// - /// Using the Thread.Sleep(15) mechanism. - /// - ThreadSleep, - - /// - /// Using the Task.Delay(1).Wait mechanism. - /// - TaskDelay, - - /// - /// Using a wait event that completes in a background ThreadPool thread. - /// - ThreadPool, - } - - /// - /// Gets the selected delay strategy. - /// - public DelayStrategy Strategy { get; } - - /// - /// Creates the smallest possible, synchronous delay based on the selected strategy. - /// - /// The elapsed time of the delay. - public TimeSpan WaitOne() - { - lock (_syncRoot) - { - if (_isDisposed) return TimeSpan.Zero; - - _delayStopwatch.Restart(); - - switch (Strategy) - { - case DelayStrategy.ThreadSleep: - DelaySleep(); - break; - case DelayStrategy.TaskDelay: - DelayTask(); - break; -#if !NETSTANDARD1_3 - case DelayStrategy.ThreadPool: - DelayThreadPool(); - break; + /// The strategy. + public DelayProvider(DelayStrategy strategy = DelayStrategy.TaskDelay) => this.Strategy = strategy; + + /// + /// Enumerates the different ways of providing delays. + /// + public enum DelayStrategy { + /// + /// Using the Thread.Sleep(15) mechanism. + /// + ThreadSleep, + + /// + /// Using the Task.Delay(1).Wait mechanism. + /// + TaskDelay, + + /// + /// Using a wait event that completes in a background ThreadPool thread. + /// + ThreadPool, + } + + /// + /// Gets the selected delay strategy. + /// + public DelayStrategy Strategy { + get; + } + + /// + /// Creates the smallest possible, synchronous delay based on the selected strategy. + /// + /// The elapsed time of the delay. + public TimeSpan WaitOne() { + lock(this._syncRoot) { + if(this._isDisposed) { + return TimeSpan.Zero; + } + + this._delayStopwatch.Restart(); + + switch(this.Strategy) { + case DelayStrategy.ThreadSleep: + DelaySleep(); + break; + case DelayStrategy.TaskDelay: + DelayTask(); + break; +#if !NETSTANDARD1_3 + case DelayStrategy.ThreadPool: + this.DelayThreadPool(); + break; #endif - } - - return _delayStopwatch.Elapsed; - } - } - - #region Dispose Pattern - - /// - public void Dispose() - { - lock (_syncRoot) - { - if (_isDisposed) return; - _isDisposed = true; - _delayEvent?.Dispose(); - } - } - - #endregion - - #region Private Delay Mechanisms - - private static void DelaySleep() => Thread.Sleep(15); - - private static void DelayTask() => Task.Delay(1).Wait(); - -#if !NETSTANDARD1_3 - private void DelayThreadPool() - { - if (_delayEvent == null) - _delayEvent = WaitEventFactory.Create(isCompleted: true, useSlim: true); - - _delayEvent.Begin(); - ThreadPool.QueueUserWorkItem((s) => - { - DelaySleep(); - _delayEvent.Complete(); - }); - - _delayEvent.Wait(); - } + } + + return this._delayStopwatch.Elapsed; + } + } + + #region Dispose Pattern + + /// + public void Dispose() { + lock(this._syncRoot) { + if(this._isDisposed) { + return; + } + + this._isDisposed = true; + this._delayEvent?.Dispose(); + } + } + + #endregion + + #region Private Delay Mechanisms + + private static void DelaySleep() => Thread.Sleep(15); + + private static void DelayTask() => Task.Delay(1).Wait(); + +#if !NETSTANDARD1_3 + private void DelayThreadPool() { + if(this._delayEvent == null) { + this._delayEvent = WaitEventFactory.Create(isCompleted: true, useSlim: true); + } + + this._delayEvent.Begin(); + _ = ThreadPool.QueueUserWorkItem((s) => { + DelaySleep(); + this._delayEvent.Complete(); + }); + + this._delayEvent.Wait(); + } #endif - #endregion - } + #endregion + } } \ No newline at end of file diff --git a/Unosquare.Swan/Components/DependencyContainer.cs b/Unosquare.Swan/Components/DependencyContainer.cs index f7d0ee7..5d87ced 100644 --- a/Unosquare.Swan/Components/DependencyContainer.cs +++ b/Unosquare.Swan/Components/DependencyContainer.cs @@ -1,715 +1,638 @@ -namespace Unosquare.Swan.Components -{ - using Exceptions; - using System; - using System.Collections.Generic; - using System.Linq; - using System.Reflection; - +using Unosquare.Swan.Exceptions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Unosquare.Swan.Components { + /// + /// The concrete implementation of a simple IoC container + /// based largely on TinyIoC (https://github.com/grumpydev/TinyIoC). + /// + /// + public partial class DependencyContainer : IDisposable { + private readonly Object _autoRegisterLock = new Object(); + + private Boolean _disposed; + + static DependencyContainer() { + } + /// - /// The concrete implementation of a simple IoC container - /// based largely on TinyIoC (https://github.com/grumpydev/TinyIoC). + /// Initializes a new instance of the class. /// - /// - public partial class DependencyContainer : IDisposable - { - private readonly object _autoRegisterLock = new object(); - - private bool _disposed; - - static DependencyContainer() - { - } - - /// - /// Initializes a new instance of the class. - /// - public DependencyContainer() - { - RegisteredTypes = new TypesConcurrentDictionary(this); - Register(this); - - // Only register the TinyMessenger singleton if we are the root container - if (Parent == null) - Register(); - } - - private DependencyContainer(DependencyContainer parent) - : this() - { - Parent = parent; - } - - /// - /// Lazy created Singleton instance of the container for simple scenarios. - /// - public static DependencyContainer Current { get; } = new DependencyContainer(); - - internal DependencyContainer Parent { get; } - - internal TypesConcurrentDictionary RegisteredTypes { get; } - - /// - public void Dispose() - { - if (_disposed) return; - - _disposed = true; - - foreach (var disposable in RegisteredTypes.Values.Select(item => item as IDisposable)) - { - disposable?.Dispose(); - } - - GC.SuppressFinalize(this); - } - - /// - /// Gets the child container. - /// - /// A new instance of the class. - public DependencyContainer GetChildContainer() => new DependencyContainer(this); - - #region Registration - -#if !NETSTANDARD1_3 - /// - /// Attempt to automatically register all non-generic classes and interfaces in the current app domain. - /// Types will only be registered if they pass the supplied registration predicate. - /// - /// What action to take when encountering duplicate implementations of an interface/base class. - /// Predicate to determine if a particular type should be registered. - public void AutoRegister( - DependencyContainerDuplicateImplementationActions duplicateAction = - DependencyContainerDuplicateImplementationActions.RegisterSingle, - Func registrationPredicate = null) - { - AutoRegister( - Runtime.GetAssemblies().Where(a => !IsIgnoredAssembly(a)), - duplicateAction, - registrationPredicate); - } + public DependencyContainer() { + this.RegisteredTypes = new TypesConcurrentDictionary(this); + _ = this.Register(this); + + // Only register the TinyMessenger singleton if we are the root container + if(this.Parent == null) { + _ = this.Register(); + } + } + + private DependencyContainer(DependencyContainer parent) + : this() => this.Parent = parent; + + /// + /// Lazy created Singleton instance of the container for simple scenarios. + /// + public static DependencyContainer Current { get; } = new DependencyContainer(); + + internal DependencyContainer Parent { + get; + } + + internal TypesConcurrentDictionary RegisteredTypes { + get; + } + + /// + public void Dispose() { + if(this._disposed) { + return; + } + + this._disposed = true; + + foreach(IDisposable disposable in this.RegisteredTypes.Values.Select(item => item as IDisposable)) { + disposable?.Dispose(); + } + + GC.SuppressFinalize(this); + } + + /// + /// Gets the child container. + /// + /// A new instance of the class. + public DependencyContainer GetChildContainer() => new DependencyContainer(this); + + #region Registration + +#if !NETSTANDARD1_3 + /// + /// Attempt to automatically register all non-generic classes and interfaces in the current app domain. + /// Types will only be registered if they pass the supplied registration predicate. + /// + /// What action to take when encountering duplicate implementations of an interface/base class. + /// Predicate to determine if a particular type should be registered. + public void AutoRegister( + DependencyContainerDuplicateImplementationActions duplicateAction = + DependencyContainerDuplicateImplementationActions.RegisterSingle, + Func registrationPredicate = null) => this.AutoRegister( + Runtime.GetAssemblies().Where(a => !IsIgnoredAssembly(a)), + duplicateAction, + registrationPredicate); #endif - - /// - /// Attempt to automatically register all non-generic classes and interfaces in the specified assemblies - /// Types will only be registered if they pass the supplied registration predicate. - /// - /// Assemblies to process. - /// What action to take when encountering duplicate implementations of an interface/base class. - /// Predicate to determine if a particular type should be registered. - public void AutoRegister( - IEnumerable assemblies, - DependencyContainerDuplicateImplementationActions duplicateAction = - DependencyContainerDuplicateImplementationActions.RegisterSingle, - Func registrationPredicate = null) - { - lock (_autoRegisterLock) - { - var types = assemblies - .SelectMany(a => a.GetAllTypes()) - .Where(t => !IsIgnoredType(t, registrationPredicate)) - .ToList(); - - var concreteTypes = types - .Where(type => - type.IsClass() && !type.IsAbstract() && - (type != GetType() && (type.DeclaringType != GetType()) && !type.IsGenericTypeDefinition())) - .ToList(); - - foreach (var type in concreteTypes) - { - try - { - RegisteredTypes.Register(type, string.Empty, GetDefaultObjectFactory(type, type)); - } - catch (MethodAccessException) - { - // Ignore methods we can't access - added for Silverlight - } - } - - var abstractInterfaceTypes = types.Where( - type => - ((type.IsInterface() || type.IsAbstract()) && (type.DeclaringType != GetType()) && - (!type.IsGenericTypeDefinition()))); - - foreach (var type in abstractInterfaceTypes) - { - var localType = type; - var implementations = concreteTypes - .Where(implementationType => localType.IsAssignableFrom(implementationType)).ToList(); - - if (implementations.Skip(1).Any()) - { - if (duplicateAction == DependencyContainerDuplicateImplementationActions.Fail) - throw new DependencyContainerRegistrationException(type, implementations); - - if (duplicateAction == DependencyContainerDuplicateImplementationActions.RegisterMultiple) - { - RegisterMultiple(type, implementations); - } - } - - var firstImplementation = implementations.FirstOrDefault(); - - if (firstImplementation == null) continue; - - try - { - RegisteredTypes.Register(type, string.Empty, GetDefaultObjectFactory(type, firstImplementation)); - } - catch (MethodAccessException) - { - // Ignore methods we can't access - added for Silverlight - } - } - } - } - - /// - /// Creates/replaces a named container class registration with default options. - /// - /// Type to register. - /// Name of registration. - /// RegisterOptions for fluent API. - public RegisterOptions Register(Type registerType, string name = "") - => RegisteredTypes.Register( - registerType, - name, - GetDefaultObjectFactory(registerType, registerType)); - - /// - /// Creates/replaces a named container class registration with a given implementation and default options. - /// - /// Type to register. - /// Type to instantiate that implements RegisterType. - /// Name of registration. - /// RegisterOptions for fluent API. - public RegisterOptions Register(Type registerType, Type registerImplementation, string name = "") => - RegisteredTypes.Register(registerType, name, GetDefaultObjectFactory(registerType, registerImplementation)); - - /// - /// Creates/replaces a named container class registration with a specific, strong referenced, instance. - /// - /// Type to register. - /// Instance of RegisterType to register. - /// Name of registration. - /// RegisterOptions for fluent API. - public RegisterOptions Register(Type registerType, object instance, string name = "") => - RegisteredTypes.Register(registerType, name, new InstanceFactory(registerType, registerType, instance)); - - /// - /// Creates/replaces a named container class registration with a specific, strong referenced, instance. - /// - /// Type to register. - /// Type of instance to register that implements RegisterType. - /// Instance of RegisterImplementation to register. - /// Name of registration. - /// RegisterOptions for fluent API. - public RegisterOptions Register( - Type registerType, - Type registerImplementation, - object instance, - string name = "") - => RegisteredTypes.Register(registerType, name, new InstanceFactory(registerType, registerImplementation, instance)); - - /// - /// Creates/replaces a container class registration with a user specified factory. - /// - /// Type to register. - /// Factory/lambda that returns an instance of RegisterType. - /// Name of registration. - /// RegisterOptions for fluent API. - public RegisterOptions Register( - Type registerType, - Func, object> factory, - string name = "") - => RegisteredTypes.Register(registerType, name, new DelegateFactory(registerType, factory)); - - /// - /// Creates/replaces a named container class registration with default options. - /// - /// Type to register. - /// Name of registration. - /// RegisterOptions for fluent API. - public RegisterOptions Register(string name = "") - where TRegister : class - { - return Register(typeof(TRegister), name); - } - - /// - /// Creates/replaces a named container class registration with a given implementation and default options. - /// - /// Type to register. - /// Type to instantiate that implements RegisterType. - /// Name of registration. - /// RegisterOptions for fluent API. - public RegisterOptions Register(string name = "") - where TRegister : class - where TRegisterImplementation : class, TRegister - { - return Register(typeof(TRegister), typeof(TRegisterImplementation), name); - } - - /// - /// Creates/replaces a named container class registration with a specific, strong referenced, instance. - /// - /// Type to register. - /// Instance of RegisterType to register. - /// Name of registration. - /// RegisterOptions for fluent API. - public RegisterOptions Register(TRegister instance, string name = "") - where TRegister : class - { - return Register(typeof(TRegister), instance, name); - } - - /// - /// Creates/replaces a named container class registration with a specific, strong referenced, instance. - /// - /// Type to register. - /// Type of instance to register that implements RegisterType. - /// Instance of RegisterImplementation to register. - /// Name of registration. - /// RegisterOptions for fluent API. - public RegisterOptions Register(TRegisterImplementation instance, - string name = "") - where TRegister : class - where TRegisterImplementation : class, TRegister - { - return Register(typeof(TRegister), typeof(TRegisterImplementation), instance, name); - } - - /// - /// Creates/replaces a named container class registration with a user specified factory. - /// - /// Type to register. - /// Factory/lambda that returns an instance of RegisterType. - /// Name of registration. - /// RegisterOptions for fluent API. - public RegisterOptions Register( - Func, TRegister> factory, string name = "") - where TRegister : class - { - if (factory == null) - { - throw new ArgumentNullException(nameof(factory)); - } - - return Register(typeof(TRegister), factory, name); - } - - /// - /// Register multiple implementations of a type. - /// - /// Internally this registers each implementation using the full name of the class as its registration name. - /// - /// Type that each implementation implements. - /// Types that implement RegisterType. - /// MultiRegisterOptions for the fluent API. - public MultiRegisterOptions RegisterMultiple(IEnumerable implementationTypes) => - RegisterMultiple(typeof(TRegister), implementationTypes); - - /// - /// Register multiple implementations of a type. - /// - /// Internally this registers each implementation using the full name of the class as its registration name. - /// - /// Type that each implementation implements. - /// Types that implement RegisterType. - /// MultiRegisterOptions for the fluent API. - public MultiRegisterOptions RegisterMultiple(Type registrationType, IEnumerable implementationTypes) - { - if (implementationTypes == null) - throw new ArgumentNullException(nameof(implementationTypes), "types is null."); - - foreach (var type in implementationTypes.Where(type => !registrationType.IsAssignableFrom(type))) - { - throw new ArgumentException( - $"types: The type {registrationType.FullName} is not assignable from {type.FullName}"); - } - - if (implementationTypes.Count() != implementationTypes.Distinct().Count()) - { - var queryForDuplicatedTypes = implementationTypes - .GroupBy(i => i) - .Where(j => j.Count() > 1) - .Select(j => j.Key.FullName); - - var fullNamesOfDuplicatedTypes = string.Join(",\n", queryForDuplicatedTypes.ToArray()); - - throw new ArgumentException( - $"types: The same implementation type cannot be specified multiple times for {registrationType.FullName}\n\n{fullNamesOfDuplicatedTypes}"); - } - - var registerOptions = implementationTypes - .Select(type => Register(registrationType, type, type.FullName)) - .ToList(); - - return new MultiRegisterOptions(registerOptions); - } - - #endregion - - #region Unregistration - - /// - /// Remove a named container class registration. - /// - /// Type to unregister. - /// Name of registration. - /// true if the registration is successfully found and removed; otherwise, false. - public bool Unregister(string name = "") => Unregister(typeof(TRegister), name); - - /// - /// Remove a named container class registration. - /// - /// Type to unregister. - /// Name of registration. - /// true if the registration is successfully found and removed; otherwise, false. - public bool Unregister(Type registerType, string name = "") => - RegisteredTypes.RemoveRegistration(new TypeRegistration(registerType, name)); - - #endregion - - #region Resolution - - /// - /// Attempts to resolve a named type using specified options and the supplied constructor parameters. - /// - /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). - /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. - /// - /// Type to resolve. - /// Name of registration. - /// Resolution options. - /// Instance of type. - /// Unable to resolve the type. - public object Resolve( - Type resolveType, - string name = null, - DependencyContainerResolveOptions options = null) - => RegisteredTypes.ResolveInternal(new TypeRegistration(resolveType, name), options ?? DependencyContainerResolveOptions.Default); - - /// - /// Attempts to resolve a named type using specified options and the supplied constructor parameters. - /// - /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). - /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. - /// - /// Type to resolve. - /// Name of registration. - /// Resolution options. - /// Instance of type. - /// Unable to resolve the type. - public TResolveType Resolve( - string name = null, - DependencyContainerResolveOptions options = null) - where TResolveType : class - { - return (TResolveType)Resolve(typeof(TResolveType), name, options); - } - - /// - /// Attempts to predict whether a given type can be resolved with the supplied constructor parameters options. - /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). - /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. - /// Note: Resolution may still fail if user defined factory registrations fail to construct objects when called. - /// - /// Type to resolve. - /// The name. - /// Resolution options. - /// - /// Bool indicating whether the type can be resolved. - /// - public bool CanResolve( - Type resolveType, - string name = null, - DependencyContainerResolveOptions options = null) => - RegisteredTypes.CanResolve(new TypeRegistration(resolveType, name), options); - - /// - /// Attempts to predict whether a given named type can be resolved with the supplied constructor parameters options. - /// - /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). - /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. - /// - /// Note: Resolution may still fail if user defined factory registrations fail to construct objects when called. - /// - /// Type to resolve. - /// Name of registration. - /// Resolution options. - /// Bool indicating whether the type can be resolved. - public bool CanResolve( - string name = null, - DependencyContainerResolveOptions options = null) - where TResolveType : class - { - return CanResolve(typeof(TResolveType), name, options); - } - - /// - /// Attempts to resolve a type using the default options. - /// - /// Type to resolve. - /// Resolved type or default if resolve fails. - /// true if resolved successfully, false otherwise. - public bool TryResolve(Type resolveType, out object resolvedType) - { - try - { - resolvedType = Resolve(resolveType); - return true; - } - catch (DependencyContainerResolutionException) - { - resolvedType = null; - return false; - } - } - - /// - /// Attempts to resolve a type using the given options. - /// - /// Type to resolve. - /// Resolution options. - /// Resolved type or default if resolve fails. - /// true if resolved successfully, false otherwise. - public bool TryResolve(Type resolveType, DependencyContainerResolveOptions options, out object resolvedType) - { - try - { - resolvedType = Resolve(resolveType, options: options); - return true; - } - catch (DependencyContainerResolutionException) - { - resolvedType = null; - return false; - } - } - - /// - /// Attempts to resolve a type using the default options and given name. - /// - /// Type to resolve. - /// Name of registration. - /// Resolved type or default if resolve fails. - /// true if resolved successfully, false otherwise. - public bool TryResolve(Type resolveType, string name, out object resolvedType) - { - try - { - resolvedType = Resolve(resolveType, name); - return true; - } - catch (DependencyContainerResolutionException) - { - resolvedType = null; - return false; - } - } - - /// - /// Attempts to resolve a type using the given options and name. - /// - /// Type to resolve. - /// Name of registration. - /// Resolution options. - /// Resolved type or default if resolve fails. - /// true if resolved successfully, false otherwise. - public bool TryResolve( - Type resolveType, - string name, - DependencyContainerResolveOptions options, - out object resolvedType) - { - try - { - resolvedType = Resolve(resolveType, name, options); - return true; - } - catch (DependencyContainerResolutionException) - { - resolvedType = null; - return false; - } - } - - /// - /// Attempts to resolve a type using the default options. - /// - /// Type to resolve. - /// Resolved type or default if resolve fails. - /// true if resolved successfully, false otherwise. - public bool TryResolve(out TResolveType resolvedType) - where TResolveType : class - { - try - { - resolvedType = Resolve(); - return true; - } - catch (DependencyContainerResolutionException) - { - resolvedType = default; - return false; - } - } - - /// - /// Attempts to resolve a type using the given options. - /// - /// Type to resolve. - /// Resolution options. - /// Resolved type or default if resolve fails. - /// true if resolved successfully, false otherwise. - public bool TryResolve(DependencyContainerResolveOptions options, out TResolveType resolvedType) - where TResolveType : class - { - try - { - resolvedType = Resolve(options: options); - return true; - } - catch (DependencyContainerResolutionException) - { - resolvedType = default; - return false; - } - } - - /// - /// Attempts to resolve a type using the default options and given name. - /// - /// Type to resolve. - /// Name of registration. - /// Resolved type or default if resolve fails. - /// true if resolved successfully, false otherwise. - public bool TryResolve(string name, out TResolveType resolvedType) - where TResolveType : class - { - try - { - resolvedType = Resolve(name); - return true; - } - catch (DependencyContainerResolutionException) - { - resolvedType = default; - return false; - } - } - - /// - /// Attempts to resolve a type using the given options and name. - /// - /// Type to resolve. - /// Name of registration. - /// Resolution options. - /// Resolved type or default if resolve fails. - /// true if resolved successfully, false otherwise. - public bool TryResolve( - string name, - DependencyContainerResolveOptions options, - out TResolveType resolvedType) - where TResolveType : class - { - try - { - resolvedType = Resolve(name, options); - return true; - } - catch (DependencyContainerResolutionException) - { - resolvedType = default; - return false; - } - } - - /// - /// Returns all registrations of a type. - /// - /// Type to resolveAll. - /// Whether to include un-named (default) registrations. - /// IEnumerable. - public IEnumerable ResolveAll(Type resolveType, bool includeUnnamed = false) - => RegisteredTypes.Resolve(resolveType, includeUnnamed); - - /// - /// Returns all registrations of a type. - /// - /// Type to resolveAll. - /// Whether to include un-named (default) registrations. - /// IEnumerable. - public IEnumerable ResolveAll(bool includeUnnamed = true) - where TResolveType : class - { - return ResolveAll(typeof(TResolveType), includeUnnamed).Cast(); - } - - /// - /// Attempts to resolve all public property dependencies on the given object using the given resolve options. - /// - /// Object to "build up". - /// Resolve options to use. - public void BuildUp(object input, DependencyContainerResolveOptions resolveOptions = null) - { - if (resolveOptions == null) - resolveOptions = DependencyContainerResolveOptions.Default; - - var properties = input.GetType() - .GetProperties() - .Where(property => property.GetCacheGetMethod() != null && property.GetCacheSetMethod() != null && - !property.PropertyType.IsValueType()); - - foreach (var property in properties.Where(property => property.GetValue(input, null) == null)) - { - try - { - property.SetValue( - input, - RegisteredTypes.ResolveInternal(new TypeRegistration(property.PropertyType), resolveOptions), - null); - } - catch (DependencyContainerResolutionException) - { - // Catch any resolution errors and ignore them - } - } - } - - #endregion - - #region Internal Methods - - internal static bool IsValidAssignment(Type registerType, Type registerImplementation) - { - if (!registerType.IsGenericTypeDefinition()) - { - if (!registerType.IsAssignableFrom(registerImplementation)) - return false; - } - else - { - if (registerType.IsInterface() && registerImplementation.GetInterfaces().All(t => t.Name != registerType.Name)) - return false; - - if (registerType.IsAbstract() && registerImplementation.BaseType() != registerType) - return false; - } - - return true; - } - -#if !NETSTANDARD1_3 - private static bool IsIgnoredAssembly(Assembly assembly) - { - // TODO - find a better way to remove "system" assemblies from the auto registration - var ignoreChecks = new List> - { + + /// + /// Attempt to automatically register all non-generic classes and interfaces in the specified assemblies + /// Types will only be registered if they pass the supplied registration predicate. + /// + /// Assemblies to process. + /// What action to take when encountering duplicate implementations of an interface/base class. + /// Predicate to determine if a particular type should be registered. + public void AutoRegister( + IEnumerable assemblies, + DependencyContainerDuplicateImplementationActions duplicateAction = + DependencyContainerDuplicateImplementationActions.RegisterSingle, + Func registrationPredicate = null) { + lock(this._autoRegisterLock) { + List types = assemblies + .SelectMany(a => a.GetAllTypes()) + .Where(t => !IsIgnoredType(t, registrationPredicate)) + .ToList(); + + List concreteTypes = types + .Where(type => + type.IsClass() && !type.IsAbstract() && + type != this.GetType() && type.DeclaringType != this.GetType() && !type.IsGenericTypeDefinition()) + .ToList(); + + foreach(Type type in concreteTypes) { + try { + _ = this.RegisteredTypes.Register(type, String.Empty, GetDefaultObjectFactory(type, type)); + } catch(MethodAccessException) { + // Ignore methods we can't access - added for Silverlight + } + } + + IEnumerable abstractInterfaceTypes = types.Where( + type => + (type.IsInterface() || type.IsAbstract()) && type.DeclaringType != this.GetType() && + !type.IsGenericTypeDefinition()); + + foreach(Type type in abstractInterfaceTypes) { + Type localType = type; + List implementations = concreteTypes + .Where(implementationType => localType.IsAssignableFrom(implementationType)).ToList(); + + if(implementations.Skip(1).Any()) { + if(duplicateAction == DependencyContainerDuplicateImplementationActions.Fail) { + throw new DependencyContainerRegistrationException(type, implementations); + } + + if(duplicateAction == DependencyContainerDuplicateImplementationActions.RegisterMultiple) { + _ = this.RegisterMultiple(type, implementations); + } + } + + Type firstImplementation = implementations.FirstOrDefault(); + + if(firstImplementation == null) { + continue; + } + + try { + _ = this.RegisteredTypes.Register(type, String.Empty, GetDefaultObjectFactory(type, firstImplementation)); + } catch(MethodAccessException) { + // Ignore methods we can't access - added for Silverlight + } + } + } + } + + /// + /// Creates/replaces a named container class registration with default options. + /// + /// Type to register. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register(Type registerType, String name = "") + => this.RegisteredTypes.Register( + registerType, + name, + GetDefaultObjectFactory(registerType, registerType)); + + /// + /// Creates/replaces a named container class registration with a given implementation and default options. + /// + /// Type to register. + /// Type to instantiate that implements RegisterType. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register(Type registerType, Type registerImplementation, String name = "") => + this.RegisteredTypes.Register(registerType, name, GetDefaultObjectFactory(registerType, registerImplementation)); + + /// + /// Creates/replaces a named container class registration with a specific, strong referenced, instance. + /// + /// Type to register. + /// Instance of RegisterType to register. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register(Type registerType, Object instance, String name = "") => + this.RegisteredTypes.Register(registerType, name, new InstanceFactory(registerType, registerType, instance)); + + /// + /// Creates/replaces a named container class registration with a specific, strong referenced, instance. + /// + /// Type to register. + /// Type of instance to register that implements RegisterType. + /// Instance of RegisterImplementation to register. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register( + Type registerType, + Type registerImplementation, + Object instance, + String name = "") + => this.RegisteredTypes.Register(registerType, name, new InstanceFactory(registerType, registerImplementation, instance)); + + /// + /// Creates/replaces a container class registration with a user specified factory. + /// + /// Type to register. + /// Factory/lambda that returns an instance of RegisterType. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register( + Type registerType, + Func, Object> factory, + String name = "") + => this.RegisteredTypes.Register(registerType, name, new DelegateFactory(registerType, factory)); + + /// + /// Creates/replaces a named container class registration with default options. + /// + /// Type to register. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register(String name = "") + where TRegister : class => this.Register(typeof(TRegister), name); + + /// + /// Creates/replaces a named container class registration with a given implementation and default options. + /// + /// Type to register. + /// Type to instantiate that implements RegisterType. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register(String name = "") + where TRegister : class + where TRegisterImplementation : class, TRegister => this.Register(typeof(TRegister), typeof(TRegisterImplementation), name); + + /// + /// Creates/replaces a named container class registration with a specific, strong referenced, instance. + /// + /// Type to register. + /// Instance of RegisterType to register. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register(TRegister instance, String name = "") + where TRegister : class => this.Register(typeof(TRegister), instance, name); + + /// + /// Creates/replaces a named container class registration with a specific, strong referenced, instance. + /// + /// Type to register. + /// Type of instance to register that implements RegisterType. + /// Instance of RegisterImplementation to register. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register(TRegisterImplementation instance, + String name = "") + where TRegister : class + where TRegisterImplementation : class, TRegister => this.Register(typeof(TRegister), typeof(TRegisterImplementation), instance, name); + + /// + /// Creates/replaces a named container class registration with a user specified factory. + /// + /// Type to register. + /// Factory/lambda that returns an instance of RegisterType. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register( + Func, TRegister> factory, String name = "") + where TRegister : class { + if(factory == null) { + throw new ArgumentNullException(nameof(factory)); + } + + return this.Register(typeof(TRegister), factory, name); + } + + /// + /// Register multiple implementations of a type. + /// + /// Internally this registers each implementation using the full name of the class as its registration name. + /// + /// Type that each implementation implements. + /// Types that implement RegisterType. + /// MultiRegisterOptions for the fluent API. + public MultiRegisterOptions RegisterMultiple(IEnumerable implementationTypes) => + this.RegisterMultiple(typeof(TRegister), implementationTypes); + + /// + /// Register multiple implementations of a type. + /// + /// Internally this registers each implementation using the full name of the class as its registration name. + /// + /// Type that each implementation implements. + /// Types that implement RegisterType. + /// MultiRegisterOptions for the fluent API. + public MultiRegisterOptions RegisterMultiple(Type registrationType, IEnumerable implementationTypes) { + if(implementationTypes == null) { + throw new ArgumentNullException(nameof(implementationTypes), "types is null."); + } + + foreach(Type type in implementationTypes.Where(type => !registrationType.IsAssignableFrom(type))) { + throw new ArgumentException( + $"types: The type {registrationType.FullName} is not assignable from {type.FullName}"); + } + + if(implementationTypes.Count() != implementationTypes.Distinct().Count()) { + IEnumerable queryForDuplicatedTypes = implementationTypes + .GroupBy(i => i) + .Where(j => j.Count() > 1) + .Select(j => j.Key.FullName); + + String fullNamesOfDuplicatedTypes = String.Join(",\n", queryForDuplicatedTypes.ToArray()); + + throw new ArgumentException( + $"types: The same implementation type cannot be specified multiple times for {registrationType.FullName}\n\n{fullNamesOfDuplicatedTypes}"); + } + + List registerOptions = implementationTypes + .Select(type => this.Register(registrationType, type, type.FullName)) + .ToList(); + + return new MultiRegisterOptions(registerOptions); + } + + #endregion + + #region Unregistration + + /// + /// Remove a named container class registration. + /// + /// Type to unregister. + /// Name of registration. + /// true if the registration is successfully found and removed; otherwise, false. + public Boolean Unregister(String name = "") => this.Unregister(typeof(TRegister), name); + + /// + /// Remove a named container class registration. + /// + /// Type to unregister. + /// Name of registration. + /// true if the registration is successfully found and removed; otherwise, false. + public Boolean Unregister(Type registerType, String name = "") => + this.RegisteredTypes.RemoveRegistration(new TypeRegistration(registerType, name)); + + #endregion + + #region Resolution + + /// + /// Attempts to resolve a named type using specified options and the supplied constructor parameters. + /// + /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). + /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. + /// + /// Type to resolve. + /// Name of registration. + /// Resolution options. + /// Instance of type. + /// Unable to resolve the type. + public Object Resolve( + Type resolveType, + String name = null, + DependencyContainerResolveOptions options = null) + => this.RegisteredTypes.ResolveInternal(new TypeRegistration(resolveType, name), options ?? DependencyContainerResolveOptions.Default); + + /// + /// Attempts to resolve a named type using specified options and the supplied constructor parameters. + /// + /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). + /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. + /// + /// Type to resolve. + /// Name of registration. + /// Resolution options. + /// Instance of type. + /// Unable to resolve the type. + public TResolveType Resolve( + String name = null, + DependencyContainerResolveOptions options = null) + where TResolveType : class => (TResolveType)this.Resolve(typeof(TResolveType), name, options); + + /// + /// Attempts to predict whether a given type can be resolved with the supplied constructor parameters options. + /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). + /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. + /// Note: Resolution may still fail if user defined factory registrations fail to construct objects when called. + /// + /// Type to resolve. + /// The name. + /// Resolution options. + /// + /// Bool indicating whether the type can be resolved. + /// + public Boolean CanResolve( + Type resolveType, + String name = null, + DependencyContainerResolveOptions options = null) => + this.RegisteredTypes.CanResolve(new TypeRegistration(resolveType, name), options); + + /// + /// Attempts to predict whether a given named type can be resolved with the supplied constructor parameters options. + /// + /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). + /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. + /// + /// Note: Resolution may still fail if user defined factory registrations fail to construct objects when called. + /// + /// Type to resolve. + /// Name of registration. + /// Resolution options. + /// Bool indicating whether the type can be resolved. + public Boolean CanResolve( + String name = null, + DependencyContainerResolveOptions options = null) + where TResolveType : class => this.CanResolve(typeof(TResolveType), name, options); + + /// + /// Attempts to resolve a type using the default options. + /// + /// Type to resolve. + /// Resolved type or default if resolve fails. + /// true if resolved successfully, false otherwise. + public Boolean TryResolve(Type resolveType, out Object resolvedType) { + try { + resolvedType = this.Resolve(resolveType); + return true; + } catch(DependencyContainerResolutionException) { + resolvedType = null; + return false; + } + } + + /// + /// Attempts to resolve a type using the given options. + /// + /// Type to resolve. + /// Resolution options. + /// Resolved type or default if resolve fails. + /// true if resolved successfully, false otherwise. + public Boolean TryResolve(Type resolveType, DependencyContainerResolveOptions options, out Object resolvedType) { + try { + resolvedType = this.Resolve(resolveType, options: options); + return true; + } catch(DependencyContainerResolutionException) { + resolvedType = null; + return false; + } + } + + /// + /// Attempts to resolve a type using the default options and given name. + /// + /// Type to resolve. + /// Name of registration. + /// Resolved type or default if resolve fails. + /// true if resolved successfully, false otherwise. + public Boolean TryResolve(Type resolveType, String name, out Object resolvedType) { + try { + resolvedType = this.Resolve(resolveType, name); + return true; + } catch(DependencyContainerResolutionException) { + resolvedType = null; + return false; + } + } + + /// + /// Attempts to resolve a type using the given options and name. + /// + /// Type to resolve. + /// Name of registration. + /// Resolution options. + /// Resolved type or default if resolve fails. + /// true if resolved successfully, false otherwise. + public Boolean TryResolve( + Type resolveType, + String name, + DependencyContainerResolveOptions options, + out Object resolvedType) { + try { + resolvedType = this.Resolve(resolveType, name, options); + return true; + } catch(DependencyContainerResolutionException) { + resolvedType = null; + return false; + } + } + + /// + /// Attempts to resolve a type using the default options. + /// + /// Type to resolve. + /// Resolved type or default if resolve fails. + /// true if resolved successfully, false otherwise. + public Boolean TryResolve(out TResolveType resolvedType) + where TResolveType : class { + try { + resolvedType = this.Resolve(); + return true; + } catch(DependencyContainerResolutionException) { + resolvedType = default; + return false; + } + } + + /// + /// Attempts to resolve a type using the given options. + /// + /// Type to resolve. + /// Resolution options. + /// Resolved type or default if resolve fails. + /// true if resolved successfully, false otherwise. + public Boolean TryResolve(DependencyContainerResolveOptions options, out TResolveType resolvedType) + where TResolveType : class { + try { + resolvedType = this.Resolve(options: options); + return true; + } catch(DependencyContainerResolutionException) { + resolvedType = default; + return false; + } + } + + /// + /// Attempts to resolve a type using the default options and given name. + /// + /// Type to resolve. + /// Name of registration. + /// Resolved type or default if resolve fails. + /// true if resolved successfully, false otherwise. + public Boolean TryResolve(String name, out TResolveType resolvedType) + where TResolveType : class { + try { + resolvedType = this.Resolve(name); + return true; + } catch(DependencyContainerResolutionException) { + resolvedType = default; + return false; + } + } + + /// + /// Attempts to resolve a type using the given options and name. + /// + /// Type to resolve. + /// Name of registration. + /// Resolution options. + /// Resolved type or default if resolve fails. + /// true if resolved successfully, false otherwise. + public Boolean TryResolve( + String name, + DependencyContainerResolveOptions options, + out TResolveType resolvedType) + where TResolveType : class { + try { + resolvedType = this.Resolve(name, options); + return true; + } catch(DependencyContainerResolutionException) { + resolvedType = default; + return false; + } + } + + /// + /// Returns all registrations of a type. + /// + /// Type to resolveAll. + /// Whether to include un-named (default) registrations. + /// IEnumerable. + public IEnumerable ResolveAll(Type resolveType, Boolean includeUnnamed = false) + => this.RegisteredTypes.Resolve(resolveType, includeUnnamed); + + /// + /// Returns all registrations of a type. + /// + /// Type to resolveAll. + /// Whether to include un-named (default) registrations. + /// IEnumerable. + public IEnumerable ResolveAll(Boolean includeUnnamed = true) + where TResolveType : class => this.ResolveAll(typeof(TResolveType), includeUnnamed).Cast(); + + /// + /// Attempts to resolve all public property dependencies on the given object using the given resolve options. + /// + /// Object to "build up". + /// Resolve options to use. + public void BuildUp(Object input, DependencyContainerResolveOptions resolveOptions = null) { + if(resolveOptions == null) { + resolveOptions = DependencyContainerResolveOptions.Default; + } + + IEnumerable properties = input.GetType() + .GetProperties() + .Where(property => property.GetCacheGetMethod() != null && property.GetCacheSetMethod() != null && + !property.PropertyType.IsValueType()); + + foreach(PropertyInfo property in properties.Where(property => property.GetValue(input, null) == null)) { + try { + property.SetValue( + input, + this.RegisteredTypes.ResolveInternal(new TypeRegistration(property.PropertyType), resolveOptions), + null); + } catch(DependencyContainerResolutionException) { + // Catch any resolution errors and ignore them + } + } + } + + #endregion + + #region Internal Methods + + internal static Boolean IsValidAssignment(Type registerType, Type registerImplementation) { + if(!registerType.IsGenericTypeDefinition()) { + if(!registerType.IsAssignableFrom(registerImplementation)) { + return false; + } + } else { + if(registerType.IsInterface() && registerImplementation.GetInterfaces().All(t => t.Name != registerType.Name)) { + return false; + } + + if(registerType.IsAbstract() && registerImplementation.BaseType() != registerType) { + return false; + } + } + + return true; + } + +#if !NETSTANDARD1_3 + private static Boolean IsIgnoredAssembly(Assembly assembly) { + // TODO - find a better way to remove "system" assemblies from the auto registration + List> ignoreChecks = new List> + { asm => asm.FullName.StartsWith("Microsoft.", StringComparison.Ordinal), asm => asm.FullName.StartsWith("System.", StringComparison.Ordinal), asm => asm.FullName.StartsWith("System,", StringComparison.Ordinal), @@ -718,37 +641,35 @@ asm => asm.FullName.StartsWith("CR_VSTest", StringComparison.Ordinal), asm => asm.FullName.StartsWith("DevExpress.CodeRush", StringComparison.Ordinal), asm => asm.FullName.StartsWith("xunit.", StringComparison.Ordinal), - }; - - return ignoreChecks.Any(check => check(assembly)); - } + }; + + return ignoreChecks.Any(check => check(assembly)); + } #endif - - private static bool IsIgnoredType(Type type, Func registrationPredicate) - { - // TODO - find a better way to remove "system" types from the auto registration - var ignoreChecks = new List>() - { + + private static Boolean IsIgnoredType(Type type, Func registrationPredicate) { + // TODO - find a better way to remove "system" types from the auto registration + List> ignoreChecks = new List>() + { t => t.FullName?.StartsWith("System.", StringComparison.Ordinal) ?? false, t => t.FullName?.StartsWith("Microsoft.", StringComparison.Ordinal) ?? false, t => t.IsPrimitive(), t => t.IsGenericTypeDefinition(), - t => (t.GetConstructors(BindingFlags.Instance | BindingFlags.Public).Length == 0) && + t => t.GetConstructors(BindingFlags.Instance | BindingFlags.Public).Length == 0 && !(t.IsInterface() || t.IsAbstract()), - }; - - if (registrationPredicate != null) - { - ignoreChecks.Add(t => !registrationPredicate(t)); - } - - return ignoreChecks.Any(check => check(type)); - } - - private static ObjectFactoryBase GetDefaultObjectFactory(Type registerType, Type registerImplementation) => registerType.IsInterface() || registerType.IsAbstract() - ? (ObjectFactoryBase)new SingletonFactory(registerType, registerImplementation) - : new MultiInstanceFactory(registerType, registerImplementation); - - #endregion - } + }; + + if(registrationPredicate != null) { + ignoreChecks.Add(t => !registrationPredicate(t)); + } + + return ignoreChecks.Any(check => check(type)); + } + + private static ObjectFactoryBase GetDefaultObjectFactory(Type registerType, Type registerImplementation) => registerType.IsInterface() || registerType.IsAbstract() + ? (ObjectFactoryBase)new SingletonFactory(registerType, registerImplementation) + : new MultiInstanceFactory(registerType, registerImplementation); + + #endregion + } } \ No newline at end of file diff --git a/Unosquare.Swan/Components/DependencyContainerResolveOptions.cs b/Unosquare.Swan/Components/DependencyContainerResolveOptions.cs index 4089a8e..aa76e7c 100644 --- a/Unosquare.Swan/Components/DependencyContainerResolveOptions.cs +++ b/Unosquare.Swan/Components/DependencyContainerResolveOptions.cs @@ -1,114 +1,113 @@ -namespace Unosquare.Swan.Components -{ - using System.Collections.Generic; - +using System; +using System.Collections.Generic; + +namespace Unosquare.Swan.Components { + /// + /// Resolution settings. + /// + public class DependencyContainerResolveOptions { /// - /// Resolution settings. + /// Gets the default options (attempt resolution of unregistered types, fail on named resolution if name not found). /// - public class DependencyContainerResolveOptions - { - /// - /// Gets the default options (attempt resolution of unregistered types, fail on named resolution if name not found). - /// - public static DependencyContainerResolveOptions Default { get; } = new DependencyContainerResolveOptions(); - - /// - /// Gets or sets the unregistered resolution action. - /// - /// - /// The unregistered resolution action. - /// - public DependencyContainerUnregisteredResolutionActions UnregisteredResolutionAction { get; set; } = - DependencyContainerUnregisteredResolutionActions.AttemptResolve; - - /// - /// Gets or sets the named resolution failure action. - /// - /// - /// The named resolution failure action. - /// - public DependencyContainerNamedResolutionFailureActions NamedResolutionFailureAction { get; set; } = - DependencyContainerNamedResolutionFailureActions.Fail; - - /// - /// Gets the constructor parameters. - /// - /// - /// The constructor parameters. - /// - public Dictionary ConstructorParameters { get; } = new Dictionary(); - - /// - /// Clones this instance. - /// - /// - public DependencyContainerResolveOptions Clone() => new DependencyContainerResolveOptions - { - NamedResolutionFailureAction = NamedResolutionFailureAction, - UnregisteredResolutionAction = UnregisteredResolutionAction, - }; - } - + public static DependencyContainerResolveOptions Default { get; } = new DependencyContainerResolveOptions(); + /// - /// Defines Resolution actions. + /// Gets or sets the unregistered resolution action. /// - public enum DependencyContainerUnregisteredResolutionActions - { - /// - /// Attempt to resolve type, even if the type isn't registered. - /// - /// Registered types/options will always take precedence. - /// - AttemptResolve, - - /// - /// Fail resolution if type not explicitly registered - /// - Fail, - - /// - /// Attempt to resolve unregistered type if requested type is generic - /// and no registration exists for the specific generic parameters used. - /// - /// Registered types/options will always take precedence. - /// - GenericsOnly, - } - + /// + /// The unregistered resolution action. + /// + public DependencyContainerUnregisteredResolutionActions UnregisteredResolutionAction { + get; set; + } = + DependencyContainerUnregisteredResolutionActions.AttemptResolve; + /// - /// Enumerates failure actions. + /// Gets or sets the named resolution failure action. /// - public enum DependencyContainerNamedResolutionFailureActions - { - /// - /// The attempt unnamed resolution - /// - AttemptUnnamedResolution, - - /// - /// The fail - /// - Fail, - } - + /// + /// The named resolution failure action. + /// + public DependencyContainerNamedResolutionFailureActions NamedResolutionFailureAction { + get; set; + } = + DependencyContainerNamedResolutionFailureActions.Fail; + /// - /// Enumerates duplicate definition actions. + /// Gets the constructor parameters. /// - public enum DependencyContainerDuplicateImplementationActions - { - /// - /// The register single - /// - RegisterSingle, - - /// - /// The register multiple - /// - RegisterMultiple, - - /// - /// The fail - /// - Fail, - } + /// + /// The constructor parameters. + /// + public Dictionary ConstructorParameters { get; } = new Dictionary(); + + /// + /// Clones this instance. + /// + /// + public DependencyContainerResolveOptions Clone() => new DependencyContainerResolveOptions { + NamedResolutionFailureAction = NamedResolutionFailureAction, + UnregisteredResolutionAction = UnregisteredResolutionAction, + }; + } + + /// + /// Defines Resolution actions. + /// + public enum DependencyContainerUnregisteredResolutionActions { + /// + /// Attempt to resolve type, even if the type isn't registered. + /// + /// Registered types/options will always take precedence. + /// + AttemptResolve, + + /// + /// Fail resolution if type not explicitly registered + /// + Fail, + + /// + /// Attempt to resolve unregistered type if requested type is generic + /// and no registration exists for the specific generic parameters used. + /// + /// Registered types/options will always take precedence. + /// + GenericsOnly, + } + + /// + /// Enumerates failure actions. + /// + public enum DependencyContainerNamedResolutionFailureActions { + /// + /// The attempt unnamed resolution + /// + AttemptUnnamedResolution, + + /// + /// The fail + /// + Fail, + } + + /// + /// Enumerates duplicate definition actions. + /// + public enum DependencyContainerDuplicateImplementationActions { + /// + /// The register single + /// + RegisterSingle, + + /// + /// The register multiple + /// + RegisterMultiple, + + /// + /// The fail + /// + Fail, + } } \ No newline at end of file diff --git a/Unosquare.Swan/Components/IMessageHubMessage.cs b/Unosquare.Swan/Components/IMessageHubMessage.cs index 82a8927..3a3340a 100644 --- a/Unosquare.Swan/Components/IMessageHubMessage.cs +++ b/Unosquare.Swan/Components/IMessageHubMessage.cs @@ -1,13 +1,15 @@ -namespace Unosquare.Swan.Components -{ +using System; + +namespace Unosquare.Swan.Components { + /// + /// A Message to be published/delivered by Messenger. + /// + public interface IMessageHubMessage { /// - /// A Message to be published/delivered by Messenger. + /// The sender of the message, or null if not supported by the message implementation. /// - public interface IMessageHubMessage - { - /// - /// The sender of the message, or null if not supported by the message implementation. - /// - object Sender { get; } - } + Object Sender { + get; + } + } } diff --git a/Unosquare.Swan/Components/MessageHub.cs b/Unosquare.Swan/Components/MessageHub.cs index 263c3fe..e9576d7 100644 --- a/Unosquare.Swan/Components/MessageHub.cs +++ b/Unosquare.Swan/Components/MessageHub.cs @@ -1,466 +1,443 @@ -// =============================================================================== -// TinyIoC - TinyMessenger -// -// A simple messenger/event aggregator. -// -// https://github.com/grumpydev/TinyIoC/blob/master/src/TinyIoC/TinyMessenger.cs -// =============================================================================== -// Copyright © Steven Robbins. All rights reserved. -// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY -// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT -// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND -// FITNESS FOR A PARTICULAR PURPOSE. -// =============================================================================== - -namespace Unosquare.Swan.Components -{ - using System.Threading.Tasks; - using System; - using System.Collections.Generic; - using System.Linq; - - #region Message Types / Interfaces - +// =============================================================================== +// TinyIoC - TinyMessenger +// +// A simple messenger/event aggregator. +// +// https://github.com/grumpydev/TinyIoC/blob/master/src/TinyIoC/TinyMessenger.cs +// =============================================================================== +// Copyright © Steven Robbins. All rights reserved. +// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY +// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT +// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE. +// =============================================================================== + +using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Unosquare.Swan.Components { + #region Message Types / Interfaces + + /// + /// Represents a message subscription. + /// + public interface IMessageHubSubscription { /// - /// Represents a message subscription. + /// Token returned to the subscribed to reference this subscription. /// - public interface IMessageHubSubscription - { - /// - /// Token returned to the subscribed to reference this subscription. - /// - MessageHubSubscriptionToken SubscriptionToken { get; } - - /// - /// Whether delivery should be attempted. - /// - /// Message that may potentially be delivered. - /// true - ok to send, false - should not attempt to send. - bool ShouldAttemptDelivery(IMessageHubMessage message); - - /// - /// Deliver the message. - /// - /// Message to deliver. - void Deliver(IMessageHubMessage message); - } - + MessageHubSubscriptionToken SubscriptionToken { + get; + } + /// - /// Message proxy definition. + /// Whether delivery should be attempted. + /// + /// Message that may potentially be delivered. + /// true - ok to send, false - should not attempt to send. + Boolean ShouldAttemptDelivery(IMessageHubMessage message); + + /// + /// Deliver the message. + /// + /// Message to deliver. + void Deliver(IMessageHubMessage message); + } + + /// + /// Message proxy definition. + /// + /// A message proxy can be used to intercept/alter messages and/or + /// marshal delivery actions onto a particular thread. + /// + public interface IMessageHubProxy { + /// + /// Delivers the specified message. + /// + /// The message. + /// The subscription. + void Deliver(IMessageHubMessage message, IMessageHubSubscription subscription); + } + + /// + /// Default "pass through" proxy. + /// + /// Does nothing other than deliver the message. + /// + public sealed class MessageHubDefaultProxy : IMessageHubProxy { + private MessageHubDefaultProxy() { + // placeholder + } + + /// + /// Singleton instance of the proxy. + /// + public static MessageHubDefaultProxy Instance { get; } = new MessageHubDefaultProxy(); + + /// + /// Delivers the specified message. + /// + /// The message. + /// The subscription. + public void Deliver(IMessageHubMessage message, IMessageHubSubscription subscription) + => subscription.Deliver(message); + } + + #endregion + + #region Hub Interface + + /// + /// Messenger hub responsible for taking subscriptions/publications and delivering of messages. + /// + public interface IMessageHub { + /// + /// Subscribe to a message type with the given destination and delivery action. + /// Messages will be delivered via the specified proxy. /// - /// A message proxy can be used to intercept/alter messages and/or - /// marshal delivery actions onto a particular thread. + /// All messages of this type will be delivered. /// - public interface IMessageHubProxy - { - /// - /// Delivers the specified message. - /// - /// The message. - /// The subscription. - void Deliver(IMessageHubMessage message, IMessageHubSubscription subscription); - } - + /// Type of message. + /// Action to invoke when message is delivered. + /// Use strong references to destination and deliveryAction. + /// Proxy to use when delivering the messages. + /// MessageSubscription used to unsubscribing. + MessageHubSubscriptionToken Subscribe( + Action deliveryAction, + Boolean useStrongReferences, + IMessageHubProxy proxy) + where TMessage : class, IMessageHubMessage; + /// - /// Default "pass through" proxy. + /// Subscribe to a message type with the given destination and delivery action with the given filter. + /// Messages will be delivered via the specified proxy. + /// All references are held with WeakReferences + /// Only messages that "pass" the filter will be delivered. + /// + /// Type of message. + /// Action to invoke when message is delivered. + /// The message filter. + /// Use strong references to destination and deliveryAction. + /// Proxy to use when delivering the messages. + /// + /// MessageSubscription used to unsubscribing. + /// + MessageHubSubscriptionToken Subscribe( + Action deliveryAction, + Func messageFilter, + Boolean useStrongReferences, + IMessageHubProxy proxy) + where TMessage : class, IMessageHubMessage; + + /// + /// Unsubscribe from a particular message type. /// - /// Does nothing other than deliver the message. + /// Does not throw an exception if the subscription is not found. /// - public sealed class MessageHubDefaultProxy : IMessageHubProxy - { - private MessageHubDefaultProxy() - { - // placeholder - } - - /// - /// Singleton instance of the proxy. - /// - public static MessageHubDefaultProxy Instance { get; } = new MessageHubDefaultProxy(); - - /// - /// Delivers the specified message. - /// - /// The message. - /// The subscription. - public void Deliver(IMessageHubMessage message, IMessageHubSubscription subscription) - => subscription.Deliver(message); - } - - #endregion - - #region Hub Interface - + /// Type of message. + /// Subscription token received from Subscribe. + void Unsubscribe(MessageHubSubscriptionToken subscriptionToken) + where TMessage : class, IMessageHubMessage; + /// - /// Messenger hub responsible for taking subscriptions/publications and delivering of messages. + /// Publish a message to any subscribers. /// - public interface IMessageHub - { - /// - /// Subscribe to a message type with the given destination and delivery action. - /// Messages will be delivered via the specified proxy. - /// - /// All messages of this type will be delivered. - /// - /// Type of message. - /// Action to invoke when message is delivered. - /// Use strong references to destination and deliveryAction. - /// Proxy to use when delivering the messages. - /// MessageSubscription used to unsubscribing. - MessageHubSubscriptionToken Subscribe( - Action deliveryAction, - bool useStrongReferences, - IMessageHubProxy proxy) - where TMessage : class, IMessageHubMessage; - - /// - /// Subscribe to a message type with the given destination and delivery action with the given filter. - /// Messages will be delivered via the specified proxy. - /// All references are held with WeakReferences - /// Only messages that "pass" the filter will be delivered. - /// - /// Type of message. - /// Action to invoke when message is delivered. - /// The message filter. - /// Use strong references to destination and deliveryAction. - /// Proxy to use when delivering the messages. - /// - /// MessageSubscription used to unsubscribing. - /// - MessageHubSubscriptionToken Subscribe( - Action deliveryAction, - Func messageFilter, - bool useStrongReferences, - IMessageHubProxy proxy) - where TMessage : class, IMessageHubMessage; - - /// - /// Unsubscribe from a particular message type. - /// - /// Does not throw an exception if the subscription is not found. - /// - /// Type of message. - /// Subscription token received from Subscribe. - void Unsubscribe(MessageHubSubscriptionToken subscriptionToken) - where TMessage : class, IMessageHubMessage; - - /// - /// Publish a message to any subscribers. - /// - /// Type of message. - /// Message to deliver. - void Publish(TMessage message) - where TMessage : class, IMessageHubMessage; - - /// - /// Publish a message to any subscribers asynchronously. - /// - /// Type of message. - /// Message to deliver. - /// A task from Publish action. - Task PublishAsync(TMessage message) - where TMessage : class, IMessageHubMessage; - } - + /// Type of message. + /// Message to deliver. + void Publish(TMessage message) + where TMessage : class, IMessageHubMessage; + + /// + /// Publish a message to any subscribers asynchronously. + /// + /// Type of message. + /// Message to deliver. + /// A task from Publish action. + Task PublishAsync(TMessage message) + where TMessage : class, IMessageHubMessage; + } + + #endregion + + #region Hub Implementation + + /// + /// + /// The following code describes how to use a MessageHub. Both the + /// subscription and the message sending are done in the same place but this is only for explanatory purposes. + /// + /// class Example + /// { + /// using Unosquare.Swan; + /// using Unosquare.Swan.Components; + /// + /// static void Main() + /// { + /// // using DependencyContainer to create an instance of MessageHub + /// var messageHub = DependencyContainer + /// .Current + /// .Resolve<IMessageHub>() as MessageHub; + /// + /// // create an instance of the publisher class + /// // which has a string as its content + /// var message = new MessageHubGenericMessage<string>(new object(), "SWAN"); + /// + /// // subscribe to the publisher's event + /// // and just print out the content which is a string + /// // a token is returned which can be used to unsubscribe later on + /// var token = messageHub + /// .Subscribe<MessageHubGenericMessage<string>>(m => m.Content.Info()); + /// + /// // publish the message described above which is + /// // the string 'SWAN' + /// messageHub.Publish(message); + /// + /// // unsuscribe, we will no longer receive any messages + /// messageHub.Unsubscribe<MessageHubGenericMessage<string>>(token); + /// + /// Terminal.Flush(); + /// } + /// + /// } + /// + /// + public sealed class MessageHub : IMessageHub { + #region Private Types and Interfaces + + private readonly Object _subscriptionsPadlock = new Object(); + + private readonly Dictionary> _subscriptions = + new Dictionary>(); + + private class WeakMessageSubscription : IMessageHubSubscription + where TMessage : class, IMessageHubMessage { + private readonly WeakReference _deliveryAction; + private readonly WeakReference _messageFilter; + + /// + /// Initializes a new instance of the class. + /// + /// The subscription token. + /// The delivery action. + /// The message filter. + /// subscriptionToken + /// or + /// deliveryAction + /// or + /// messageFilter. + public WeakMessageSubscription( + MessageHubSubscriptionToken subscriptionToken, + Action deliveryAction, + Func messageFilter) { + this.SubscriptionToken = subscriptionToken ?? throw new ArgumentNullException(nameof(subscriptionToken)); + this._deliveryAction = new WeakReference(deliveryAction); + this._messageFilter = new WeakReference(messageFilter); + } + + public MessageHubSubscriptionToken SubscriptionToken { + get; + } + + public Boolean ShouldAttemptDelivery(IMessageHubMessage message) => this._deliveryAction.IsAlive && this._messageFilter.IsAlive && + ((Func)this._messageFilter.Target).Invoke((TMessage)message); + + public void Deliver(IMessageHubMessage message) { + if(this._deliveryAction.IsAlive) { + ((Action)this._deliveryAction.Target).Invoke((TMessage)message); + } + } + } + + private class StrongMessageSubscription : IMessageHubSubscription + where TMessage : class, IMessageHubMessage { + private readonly Action _deliveryAction; + private readonly Func _messageFilter; + + /// + /// Initializes a new instance of the class. + /// + /// The subscription token. + /// The delivery action. + /// The message filter. + /// subscriptionToken + /// or + /// deliveryAction + /// or + /// messageFilter. + public StrongMessageSubscription( + MessageHubSubscriptionToken subscriptionToken, + Action deliveryAction, + Func messageFilter) { + this.SubscriptionToken = subscriptionToken ?? throw new ArgumentNullException(nameof(subscriptionToken)); + this._deliveryAction = deliveryAction; + this._messageFilter = messageFilter; + } + + public MessageHubSubscriptionToken SubscriptionToken { + get; + } + + public Boolean ShouldAttemptDelivery(IMessageHubMessage message) => this._messageFilter.Invoke((TMessage)message); + + public void Deliver(IMessageHubMessage message) => this._deliveryAction.Invoke((TMessage)message); + } + #endregion - - #region Hub Implementation - + + #region Subscription dictionary + + private class SubscriptionItem { + public SubscriptionItem(IMessageHubProxy proxy, IMessageHubSubscription subscription) { + this.Proxy = proxy; + this.Subscription = subscription; + } + + public IMessageHubProxy Proxy { + get; + } + public IMessageHubSubscription Subscription { + get; + } + } + + #endregion + + #region Public API + + /// + /// Subscribe to a message type with the given destination and delivery action. + /// Messages will be delivered via the specified proxy. + /// + /// All messages of this type will be delivered. + /// + /// Type of message. + /// Action to invoke when message is delivered. + /// Use strong references to destination and deliveryAction. + /// Proxy to use when delivering the messages. + /// MessageSubscription used to unsubscribing. + public MessageHubSubscriptionToken Subscribe( + Action deliveryAction, + Boolean useStrongReferences = true, + IMessageHubProxy proxy = null) + where TMessage : class, IMessageHubMessage => this.Subscribe(deliveryAction, m => true, useStrongReferences, proxy); + + + /// + /// Subscribe to a message type with the given destination and delivery action with the given filter. + /// Messages will be delivered via the specified proxy. + /// All references are held with WeakReferences + /// Only messages that "pass" the filter will be delivered. + /// + /// Type of message. + /// Action to invoke when message is delivered. + /// The message filter. + /// Use strong references to destination and deliveryAction. + /// Proxy to use when delivering the messages. + /// + /// MessageSubscription used to unsubscribing. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0068:Empfohlenes Dispose-Muster verwenden", Justification = "")] + public MessageHubSubscriptionToken Subscribe( + Action deliveryAction, + Func messageFilter, + Boolean useStrongReferences = true, + IMessageHubProxy proxy = null) + where TMessage : class, IMessageHubMessage { + if(deliveryAction == null) { + throw new ArgumentNullException(nameof(deliveryAction)); + } + + if(messageFilter == null) { + throw new ArgumentNullException(nameof(messageFilter)); + } + + lock(this._subscriptionsPadlock) { + if(!this._subscriptions.TryGetValue(typeof(TMessage), out List currentSubscriptions)) { + currentSubscriptions = new List(); + this._subscriptions[typeof(TMessage)] = currentSubscriptions; + } + + MessageHubSubscriptionToken subscriptionToken = new MessageHubSubscriptionToken(this, typeof(TMessage)); + + IMessageHubSubscription subscription = useStrongReferences + ? new StrongMessageSubscription( + subscriptionToken, + deliveryAction, + messageFilter) + : (IMessageHubSubscription)new WeakMessageSubscription( + subscriptionToken, + deliveryAction, + messageFilter); + + currentSubscriptions.Add(new SubscriptionItem(proxy ?? MessageHubDefaultProxy.Instance, subscription)); + + return subscriptionToken; + } + } + /// - /// - /// The following code describes how to use a MessageHub. Both the - /// subscription and the message sending are done in the same place but this is only for explanatory purposes. - /// - /// class Example - /// { - /// using Unosquare.Swan; - /// using Unosquare.Swan.Components; - /// - /// static void Main() - /// { - /// // using DependencyContainer to create an instance of MessageHub - /// var messageHub = DependencyContainer - /// .Current - /// .Resolve<IMessageHub>() as MessageHub; - /// - /// // create an instance of the publisher class - /// // which has a string as its content - /// var message = new MessageHubGenericMessage<string>(new object(), "SWAN"); - /// - /// // subscribe to the publisher's event - /// // and just print out the content which is a string - /// // a token is returned which can be used to unsubscribe later on - /// var token = messageHub - /// .Subscribe<MessageHubGenericMessage<string>>(m => m.Content.Info()); - /// - /// // publish the message described above which is - /// // the string 'SWAN' - /// messageHub.Publish(message); - /// - /// // unsuscribe, we will no longer receive any messages - /// messageHub.Unsubscribe<MessageHubGenericMessage<string>>(token); - /// - /// Terminal.Flush(); - /// } - /// - /// } - /// - /// - public sealed class MessageHub : IMessageHub - { - #region Private Types and Interfaces - - private readonly object _subscriptionsPadlock = new object(); - - private readonly Dictionary> _subscriptions = - new Dictionary>(); - - private class WeakMessageSubscription : IMessageHubSubscription - where TMessage : class, IMessageHubMessage - { - private readonly WeakReference _deliveryAction; - private readonly WeakReference _messageFilter; - - /// - /// Initializes a new instance of the class. - /// - /// The subscription token. - /// The delivery action. - /// The message filter. - /// subscriptionToken - /// or - /// deliveryAction - /// or - /// messageFilter. - public WeakMessageSubscription( - MessageHubSubscriptionToken subscriptionToken, - Action deliveryAction, - Func messageFilter) - { - SubscriptionToken = subscriptionToken ?? throw new ArgumentNullException(nameof(subscriptionToken)); - _deliveryAction = new WeakReference(deliveryAction); - _messageFilter = new WeakReference(messageFilter); - } - - public MessageHubSubscriptionToken SubscriptionToken { get; } - - public bool ShouldAttemptDelivery(IMessageHubMessage message) - { - return _deliveryAction.IsAlive && _messageFilter.IsAlive && - ((Func) _messageFilter.Target).Invoke((TMessage) message); - } - - public void Deliver(IMessageHubMessage message) - { - if (_deliveryAction.IsAlive) - { - ((Action) _deliveryAction.Target).Invoke((TMessage) message); - } - } - } - - private class StrongMessageSubscription : IMessageHubSubscription - where TMessage : class, IMessageHubMessage - { - private readonly Action _deliveryAction; - private readonly Func _messageFilter; - - /// - /// Initializes a new instance of the class. - /// - /// The subscription token. - /// The delivery action. - /// The message filter. - /// subscriptionToken - /// or - /// deliveryAction - /// or - /// messageFilter. - public StrongMessageSubscription( - MessageHubSubscriptionToken subscriptionToken, - Action deliveryAction, - Func messageFilter) - { - SubscriptionToken = subscriptionToken ?? throw new ArgumentNullException(nameof(subscriptionToken)); - _deliveryAction = deliveryAction; - _messageFilter = messageFilter; - } - - public MessageHubSubscriptionToken SubscriptionToken { get; } - - public bool ShouldAttemptDelivery(IMessageHubMessage message) => _messageFilter.Invoke((TMessage) message); - - public void Deliver(IMessageHubMessage message) => _deliveryAction.Invoke((TMessage) message); - } - - #endregion - - #region Subscription dictionary - - private class SubscriptionItem - { - public SubscriptionItem(IMessageHubProxy proxy, IMessageHubSubscription subscription) - { - Proxy = proxy; - Subscription = subscription; - } - - public IMessageHubProxy Proxy { get; } - public IMessageHubSubscription Subscription { get; } - } - - #endregion - - #region Public API - - /// - /// Subscribe to a message type with the given destination and delivery action. - /// Messages will be delivered via the specified proxy. - /// - /// All messages of this type will be delivered. - /// - /// Type of message. - /// Action to invoke when message is delivered. - /// Use strong references to destination and deliveryAction. - /// Proxy to use when delivering the messages. - /// MessageSubscription used to unsubscribing. - public MessageHubSubscriptionToken Subscribe( - Action deliveryAction, - bool useStrongReferences = true, - IMessageHubProxy proxy = null) - where TMessage : class, IMessageHubMessage - { - return Subscribe(deliveryAction, m => true, useStrongReferences, proxy); - } - - /// - /// Subscribe to a message type with the given destination and delivery action with the given filter. - /// Messages will be delivered via the specified proxy. - /// All references are held with WeakReferences - /// Only messages that "pass" the filter will be delivered. - /// - /// Type of message. - /// Action to invoke when message is delivered. - /// The message filter. - /// Use strong references to destination and deliveryAction. - /// Proxy to use when delivering the messages. - /// - /// MessageSubscription used to unsubscribing. - /// - public MessageHubSubscriptionToken Subscribe( - Action deliveryAction, - Func messageFilter, - bool useStrongReferences = true, - IMessageHubProxy proxy = null) - where TMessage : class, IMessageHubMessage - { - if (deliveryAction == null) - throw new ArgumentNullException(nameof(deliveryAction)); - - if (messageFilter == null) - throw new ArgumentNullException(nameof(messageFilter)); - - lock (_subscriptionsPadlock) - { - if (!_subscriptions.TryGetValue(typeof(TMessage), out var currentSubscriptions)) - { - currentSubscriptions = new List(); - _subscriptions[typeof(TMessage)] = currentSubscriptions; - } - - var subscriptionToken = new MessageHubSubscriptionToken(this, typeof(TMessage)); - - IMessageHubSubscription subscription; - if (useStrongReferences) - { - subscription = new StrongMessageSubscription( - subscriptionToken, - deliveryAction, - messageFilter); - } - else - { - subscription = new WeakMessageSubscription( - subscriptionToken, - deliveryAction, - messageFilter); - } - - currentSubscriptions.Add(new SubscriptionItem(proxy ?? MessageHubDefaultProxy.Instance, subscription)); - - return subscriptionToken; - } - } - - /// - public void Unsubscribe(MessageHubSubscriptionToken subscriptionToken) - where TMessage : class, IMessageHubMessage - { - if (subscriptionToken == null) - throw new ArgumentNullException(nameof(subscriptionToken)); - - lock (_subscriptionsPadlock) - { - if (!_subscriptions.TryGetValue(typeof(TMessage), out var currentSubscriptions)) - return; - - var currentlySubscribed = currentSubscriptions - .Where(sub => ReferenceEquals(sub.Subscription.SubscriptionToken, subscriptionToken)) - .ToList(); - - currentlySubscribed.ForEach(sub => currentSubscriptions.Remove(sub)); - } - } - - /// - /// Publish a message to any subscribers. - /// - /// Type of message. - /// Message to deliver. - public void Publish(TMessage message) - where TMessage : class, IMessageHubMessage - { - if (message == null) - throw new ArgumentNullException(nameof(message)); - - List currentlySubscribed; - lock (_subscriptionsPadlock) - { - if (!_subscriptions.TryGetValue(typeof(TMessage), out var currentSubscriptions)) - return; - - currentlySubscribed = currentSubscriptions - .Where(sub => sub.Subscription.ShouldAttemptDelivery(message)) - .ToList(); - } - - currentlySubscribed.ForEach(sub => - { - try - { - sub.Proxy.Deliver(message, sub.Subscription); - } - catch - { - // Ignore any errors and carry on - } - }); - } - - /// - /// Publish a message to any subscribers asynchronously. - /// - /// Type of message. - /// Message to deliver. - /// A task with the publish. - public Task PublishAsync(TMessage message) - where TMessage : class, IMessageHubMessage - { - return Task.Run(() => Publish(message)); - } - - #endregion - } - + public void Unsubscribe(MessageHubSubscriptionToken subscriptionToken) + where TMessage : class, IMessageHubMessage { + if(subscriptionToken == null) { + throw new ArgumentNullException(nameof(subscriptionToken)); + } + + lock(this._subscriptionsPadlock) { + if(!this._subscriptions.TryGetValue(typeof(TMessage), out List currentSubscriptions)) { + return; + } + + List currentlySubscribed = currentSubscriptions + .Where(sub => ReferenceEquals(sub.Subscription.SubscriptionToken, subscriptionToken)) + .ToList(); + + currentlySubscribed.ForEach(sub => currentSubscriptions.Remove(sub)); + } + } + + /// + /// Publish a message to any subscribers. + /// + /// Type of message. + /// Message to deliver. + public void Publish(TMessage message) + where TMessage : class, IMessageHubMessage { + if(message == null) { + throw new ArgumentNullException(nameof(message)); + } + + List currentlySubscribed; + lock(this._subscriptionsPadlock) { + if(!this._subscriptions.TryGetValue(typeof(TMessage), out List currentSubscriptions)) { + return; + } + + currentlySubscribed = currentSubscriptions + .Where(sub => sub.Subscription.ShouldAttemptDelivery(message)) + .ToList(); + } + + currentlySubscribed.ForEach(sub => { + try { + sub.Proxy.Deliver(message, sub.Subscription); + } catch { + // Ignore any errors and carry on + } + }); + } + + /// + /// Publish a message to any subscribers asynchronously. + /// + /// Type of message. + /// Message to deliver. + /// A task with the publish. + public Task PublishAsync(TMessage message) + where TMessage : class, IMessageHubMessage => Task.Run(() => this.Publish(message)); + #endregion + } + + #endregion } \ No newline at end of file diff --git a/Unosquare.Swan/Components/MessageHubMessageBase.cs b/Unosquare.Swan/Components/MessageHubMessageBase.cs index 5c8c7ea..e30649e 100644 --- a/Unosquare.Swan/Components/MessageHubMessageBase.cs +++ b/Unosquare.Swan/Components/MessageHubMessageBase.cs @@ -1,59 +1,55 @@ -namespace Unosquare.Swan.Components -{ - using System; - +using System; + +namespace Unosquare.Swan.Components { + /// + /// Base class for messages that provides weak reference storage of the sender. + /// + public abstract class MessageHubMessageBase + : IMessageHubMessage { /// - /// Base class for messages that provides weak reference storage of the sender. + /// Store a WeakReference to the sender just in case anyone is daft enough to + /// keep the message around and prevent the sender from being collected. /// - public abstract class MessageHubMessageBase - : IMessageHubMessage - { - /// - /// Store a WeakReference to the sender just in case anyone is daft enough to - /// keep the message around and prevent the sender from being collected. - /// - private readonly WeakReference _sender; - - /// - /// Initializes a new instance of the class. - /// - /// The sender. - /// sender. - protected MessageHubMessageBase(object sender) - { - if (sender == null) - throw new ArgumentNullException(nameof(sender)); - - _sender = new WeakReference(sender); - } - - /// - /// The sender of the message, or null if not supported by the message implementation. - /// - public object Sender => _sender?.Target; - } - + private readonly WeakReference _sender; + /// - /// Generic message with user specified content. + /// Initializes a new instance of the class. /// - /// Content type to store. - public class MessageHubGenericMessage - : MessageHubMessageBase - { - /// - /// Initializes a new instance of the class. - /// - /// The sender. - /// The content. - public MessageHubGenericMessage(object sender, TContent content) - : base(sender) - { - Content = content; - } - - /// - /// Contents of the message. - /// - public TContent Content { get; protected set; } - } + /// The sender. + /// sender. + protected MessageHubMessageBase(Object sender) { + if(sender == null) { + throw new ArgumentNullException(nameof(sender)); + } + + this._sender = new WeakReference(sender); + } + + /// + /// The sender of the message, or null if not supported by the message implementation. + /// + public Object Sender => this._sender?.Target; + } + + /// + /// Generic message with user specified content. + /// + /// Content type to store. + public class MessageHubGenericMessage + : MessageHubMessageBase { + /// + /// Initializes a new instance of the class. + /// + /// The sender. + /// The content. + public MessageHubGenericMessage(Object sender, TContent content) + : base(sender) => this.Content = content; + + /// + /// Contents of the message. + /// + public TContent Content { + get; protected set; + } + } } \ No newline at end of file diff --git a/Unosquare.Swan/Components/MessageHubSubscriptionToken.cs b/Unosquare.Swan/Components/MessageHubSubscriptionToken.cs index bcec34e..6e1f178 100644 --- a/Unosquare.Swan/Components/MessageHubSubscriptionToken.cs +++ b/Unosquare.Swan/Components/MessageHubSubscriptionToken.cs @@ -1,54 +1,49 @@ -namespace Unosquare.Swan.Components -{ - using System; +using System; #if NETSTANDARD1_3 - using System.Reflection; +using System.Reflection; #endif +namespace Unosquare.Swan.Components { + + + /// + /// Represents an active subscription to a message. + /// + public sealed class MessageHubSubscriptionToken + : IDisposable { + private readonly WeakReference _hub; + private readonly Type _messageType; + /// - /// Represents an active subscription to a message. + /// Initializes a new instance of the class. /// - public sealed class MessageHubSubscriptionToken - : IDisposable - { - private readonly WeakReference _hub; - private readonly Type _messageType; - - /// - /// Initializes a new instance of the class. - /// - /// The hub. - /// Type of the message. - /// hub. - /// messageType. - public MessageHubSubscriptionToken(IMessageHub hub, Type messageType) - { - if (hub == null) - { - throw new ArgumentNullException(nameof(hub)); - } - - if (!typeof(IMessageHubMessage).IsAssignableFrom(messageType)) - { - throw new ArgumentOutOfRangeException(nameof(messageType)); - } - - _hub = new WeakReference(hub); - _messageType = messageType; - } - - /// - public void Dispose() - { - if (_hub.IsAlive && _hub.Target is IMessageHub hub) - { - var unsubscribeMethod = typeof(IMessageHub).GetMethod(nameof(IMessageHub.Unsubscribe), - new[] {typeof(MessageHubSubscriptionToken)}); - unsubscribeMethod = unsubscribeMethod.MakeGenericMethod(_messageType); - unsubscribeMethod.Invoke(hub, new object[] {this}); - } - - GC.SuppressFinalize(this); - } - } + /// The hub. + /// Type of the message. + /// hub. + /// messageType. + public MessageHubSubscriptionToken(IMessageHub hub, Type messageType) { + if(hub == null) { + throw new ArgumentNullException(nameof(hub)); + } + + if(!typeof(IMessageHubMessage).IsAssignableFrom(messageType)) { + throw new ArgumentOutOfRangeException(nameof(messageType)); + } + + this._hub = new WeakReference(hub); + this._messageType = messageType; + } + + /// + public void Dispose() { + if(this._hub.IsAlive && this._hub.Target is IMessageHub hub) { + System.Reflection.MethodInfo unsubscribeMethod = typeof(IMessageHub).GetMethod(nameof(IMessageHub.Unsubscribe), + new[] { typeof(MessageHubSubscriptionToken) }); + unsubscribeMethod = unsubscribeMethod.MakeGenericMethod(this._messageType); + _ = unsubscribeMethod.Invoke(hub, new Object[] { this }); + } + + GC.SuppressFinalize(this); + } + } } \ No newline at end of file diff --git a/Unosquare.Swan/Components/ObjectFactoryBase.cs b/Unosquare.Swan/Components/ObjectFactoryBase.cs index 2771c29..7b73fe3 100644 --- a/Unosquare.Swan/Components/ObjectFactoryBase.cs +++ b/Unosquare.Swan/Components/ObjectFactoryBase.cs @@ -1,424 +1,390 @@ -namespace Unosquare.Swan.Components -{ - using System; - using System.Collections.Generic; - using System.Reflection; - using Exceptions; - +using System; +using System.Collections.Generic; +using System.Reflection; +using Unosquare.Swan.Exceptions; + +namespace Unosquare.Swan.Components { + /// + /// Represents an abstract class for Object Factory. + /// + public abstract class ObjectFactoryBase { /// - /// Represents an abstract class for Object Factory. + /// Whether to assume this factory successfully constructs its objects + /// + /// Generally set to true for delegate style factories as CanResolve cannot delve + /// into the delegates they contain. /// - public abstract class ObjectFactoryBase - { - /// - /// Whether to assume this factory successfully constructs its objects - /// - /// Generally set to true for delegate style factories as CanResolve cannot delve - /// into the delegates they contain. - /// - public virtual bool AssumeConstruction => false; - - /// - /// The type the factory instantiates. - /// - public abstract Type CreatesType { get; } - - /// - /// Constructor to use, if specified. - /// - public ConstructorInfo Constructor { get; private set; } - - /// - /// Gets the singleton variant. - /// - /// - /// The singleton variant. - /// - /// singleton. - public virtual ObjectFactoryBase SingletonVariant => - throw new DependencyContainerRegistrationException(GetType(), "singleton"); - - /// - /// Gets the multi instance variant. - /// - /// - /// The multi instance variant. - /// - /// multi-instance. - public virtual ObjectFactoryBase MultiInstanceVariant => - throw new DependencyContainerRegistrationException(GetType(), "multi-instance"); - - /// - /// Gets the strong reference variant. - /// - /// - /// The strong reference variant. - /// - /// strong reference. - public virtual ObjectFactoryBase StrongReferenceVariant => - throw new DependencyContainerRegistrationException(GetType(), "strong reference"); - - /// - /// Gets the weak reference variant. - /// - /// - /// The weak reference variant. - /// - /// weak reference. - public virtual ObjectFactoryBase WeakReferenceVariant => - throw new DependencyContainerRegistrationException(GetType(), "weak reference"); - - /// - /// Create the type. - /// - /// Type user requested to be resolved. - /// Container that requested the creation. - /// The options. - /// Instance of type. - public abstract object GetObject( - Type requestedType, - DependencyContainer container, - DependencyContainerResolveOptions options); - - /// - /// Gets the factory for child container. - /// - /// The type. - /// The parent. - /// The child. - /// - public virtual ObjectFactoryBase GetFactoryForChildContainer( - Type type, - DependencyContainer parent, - DependencyContainer child) - { - return this; - } - } - - /// + public virtual Boolean AssumeConstruction => false; + /// - /// IObjectFactory that creates new instances of types for each resolution. + /// The type the factory instantiates. /// - internal class MultiInstanceFactory : ObjectFactoryBase - { - private readonly Type _registerType; - private readonly Type _registerImplementation; - - public MultiInstanceFactory(Type registerType, Type registerImplementation) - { - if (registerImplementation.IsAbstract() || registerImplementation.IsInterface()) - { - throw new DependencyContainerRegistrationException(registerImplementation, - "MultiInstanceFactory", - true); - } - - if (!DependencyContainer.IsValidAssignment(registerType, registerImplementation)) - { - throw new DependencyContainerRegistrationException(registerImplementation, - "MultiInstanceFactory", - true); - } - - _registerType = registerType; - _registerImplementation = registerImplementation; - } - - public override Type CreatesType => _registerImplementation; - - public override ObjectFactoryBase SingletonVariant => - new SingletonFactory(_registerType, _registerImplementation); - - public override ObjectFactoryBase MultiInstanceVariant => this; - - public override object GetObject( - Type requestedType, - DependencyContainer container, - DependencyContainerResolveOptions options) - { - try - { - return container.RegisteredTypes.ConstructType(_registerImplementation, Constructor, options); - } - catch (DependencyContainerResolutionException ex) - { - throw new DependencyContainerResolutionException(_registerType, ex); - } - } - } - - /// + public abstract Type CreatesType { + get; + } + /// - /// IObjectFactory that invokes a specified delegate to construct the object. + /// Constructor to use, if specified. /// - internal class DelegateFactory : ObjectFactoryBase - { - private readonly Type _registerType; - - private readonly Func, object> _factory; - - public DelegateFactory( - Type registerType, - Func, object> factory) - { - _factory = factory ?? throw new ArgumentNullException(nameof(factory)); - - _registerType = registerType; - } - - public override bool AssumeConstruction => true; - - public override Type CreatesType => _registerType; - - public override ObjectFactoryBase WeakReferenceVariant => new WeakDelegateFactory(_registerType, _factory); - - public override ObjectFactoryBase StrongReferenceVariant => this; - - public override object GetObject( - Type requestedType, - DependencyContainer container, - DependencyContainerResolveOptions options) - { - try - { - return _factory.Invoke(container, options.ConstructorParameters); - } - catch (Exception ex) - { - throw new DependencyContainerResolutionException(_registerType, ex); - } - } - } - - /// + public ConstructorInfo Constructor { + get; private set; + } + /// - /// IObjectFactory that invokes a specified delegate to construct the object - /// Holds the delegate using a weak reference. + /// Gets the singleton variant. /// - internal class WeakDelegateFactory : ObjectFactoryBase - { - private readonly Type _registerType; - - private readonly WeakReference _factory; - - public WeakDelegateFactory(Type registerType, - Func, object> factory) - { - if (factory == null) - throw new ArgumentNullException(nameof(factory)); - - _factory = new WeakReference(factory); - - _registerType = registerType; - } - - public override bool AssumeConstruction => true; - - public override Type CreatesType => _registerType; - - public override ObjectFactoryBase StrongReferenceVariant - { - get - { - if (!(_factory.Target is Func, object> factory)) - throw new DependencyContainerWeakReferenceException(_registerType); - - return new DelegateFactory(_registerType, factory); - } - } - - public override ObjectFactoryBase WeakReferenceVariant => this; - - public override object GetObject( - Type requestedType, - DependencyContainer container, - DependencyContainerResolveOptions options) - { - if (!(_factory.Target is Func, object> factory)) - throw new DependencyContainerWeakReferenceException(_registerType); - - try - { - return factory.Invoke(container, options.ConstructorParameters); - } - catch (Exception ex) - { - throw new DependencyContainerResolutionException(_registerType, ex); - } - } - } - + /// + /// The singleton variant. + /// + /// singleton. + public virtual ObjectFactoryBase SingletonVariant => + throw new DependencyContainerRegistrationException(this.GetType(), "singleton"); + /// - /// Stores an particular instance to return for a type. + /// Gets the multi instance variant. /// - internal class InstanceFactory : ObjectFactoryBase, IDisposable - { - private readonly Type _registerType; - private readonly Type _registerImplementation; - private readonly object _instance; - - public InstanceFactory(Type registerType, Type registerImplementation, object instance) - { - if (!DependencyContainer.IsValidAssignment(registerType, registerImplementation)) - throw new DependencyContainerRegistrationException(registerImplementation, "InstanceFactory", true); - - _registerType = registerType; - _registerImplementation = registerImplementation; - _instance = instance; - } - - public override bool AssumeConstruction => true; - - public override Type CreatesType => _registerImplementation; - - public override ObjectFactoryBase MultiInstanceVariant => - new MultiInstanceFactory(_registerType, _registerImplementation); - - public override ObjectFactoryBase WeakReferenceVariant => - new WeakInstanceFactory(_registerType, _registerImplementation, _instance); - - public override ObjectFactoryBase StrongReferenceVariant => this; - - public override object GetObject( - Type requestedType, - DependencyContainer container, - DependencyContainerResolveOptions options) - { - return _instance; - } - - public void Dispose() - { - var disposable = _instance as IDisposable; - - disposable?.Dispose(); - } - } - + /// + /// The multi instance variant. + /// + /// multi-instance. + public virtual ObjectFactoryBase MultiInstanceVariant => + throw new DependencyContainerRegistrationException(this.GetType(), "multi-instance"); + /// - /// Stores the instance with a weak reference. + /// Gets the strong reference variant. /// - internal class WeakInstanceFactory : ObjectFactoryBase, IDisposable - { - private readonly Type _registerType; - private readonly Type _registerImplementation; - private readonly WeakReference _instance; - - public WeakInstanceFactory(Type registerType, Type registerImplementation, object instance) - { - if (!DependencyContainer.IsValidAssignment(registerType, registerImplementation)) - { - throw new DependencyContainerRegistrationException( - registerImplementation, - "WeakInstanceFactory", - true); - } - - _registerType = registerType; - _registerImplementation = registerImplementation; - _instance = new WeakReference(instance); - } - - public override Type CreatesType => _registerImplementation; - - public override ObjectFactoryBase MultiInstanceVariant => - new MultiInstanceFactory(_registerType, _registerImplementation); - - public override ObjectFactoryBase WeakReferenceVariant => this; - - public override ObjectFactoryBase StrongReferenceVariant - { - get - { - var instance = _instance.Target; - - if (instance == null) - throw new DependencyContainerWeakReferenceException(_registerType); - - return new InstanceFactory(_registerType, _registerImplementation, instance); - } - } - - public override object GetObject( - Type requestedType, - DependencyContainer container, - DependencyContainerResolveOptions options) - { - var instance = _instance.Target; - - if (instance == null) - throw new DependencyContainerWeakReferenceException(_registerType); - - return instance; - } - - public void Dispose() => (_instance.Target as IDisposable)?.Dispose(); - } - + /// + /// The strong reference variant. + /// + /// strong reference. + public virtual ObjectFactoryBase StrongReferenceVariant => + throw new DependencyContainerRegistrationException(this.GetType(), "strong reference"); + /// - /// A factory that lazy instantiates a type and always returns the same instance. + /// Gets the weak reference variant. /// - internal class SingletonFactory : ObjectFactoryBase, IDisposable - { - private readonly Type _registerType; - private readonly Type _registerImplementation; - private readonly object _singletonLock = new object(); - private object _current; - - public SingletonFactory(Type registerType, Type registerImplementation) - { - if (registerImplementation.IsAbstract() || registerImplementation.IsInterface()) - { - throw new DependencyContainerRegistrationException(registerImplementation, nameof(SingletonFactory), true); - } - - if (!DependencyContainer.IsValidAssignment(registerType, registerImplementation)) - { - throw new DependencyContainerRegistrationException(registerImplementation, nameof(SingletonFactory), true); - } - - _registerType = registerType; - _registerImplementation = registerImplementation; - } - - public override Type CreatesType => _registerImplementation; - - public override ObjectFactoryBase SingletonVariant => this; - - public override ObjectFactoryBase MultiInstanceVariant => - new MultiInstanceFactory(_registerType, _registerImplementation); - - public override object GetObject( - Type requestedType, - DependencyContainer container, - DependencyContainerResolveOptions options) - { - if (options.ConstructorParameters.Count != 0) - throw new ArgumentException("Cannot specify parameters for singleton types"); - - lock (_singletonLock) - { - if (_current == null) - _current = container.RegisteredTypes.ConstructType(_registerImplementation, Constructor, options); - } - - return _current; - } - - public override ObjectFactoryBase GetFactoryForChildContainer( - Type type, - DependencyContainer parent, - DependencyContainer child) - { - // We make sure that the singleton is constructed before the child container takes the factory. - // Otherwise the results would vary depending on whether or not the parent container had resolved - // the type before the child container does. - GetObject(type, parent, DependencyContainerResolveOptions.Default); - return this; - } - - public void Dispose() => (_current as IDisposable)?.Dispose(); - } + /// + /// The weak reference variant. + /// + /// weak reference. + public virtual ObjectFactoryBase WeakReferenceVariant => + throw new DependencyContainerRegistrationException(this.GetType(), "weak reference"); + + /// + /// Create the type. + /// + /// Type user requested to be resolved. + /// Container that requested the creation. + /// The options. + /// Instance of type. + public abstract Object GetObject( + Type requestedType, + DependencyContainer container, + DependencyContainerResolveOptions options); + + /// + /// Gets the factory for child container. + /// + /// The type. + /// The parent. + /// The child. + /// + public virtual ObjectFactoryBase GetFactoryForChildContainer( + Type type, + DependencyContainer parent, + DependencyContainer child) => this; + } + + /// + /// + /// IObjectFactory that creates new instances of types for each resolution. + /// + internal class MultiInstanceFactory : ObjectFactoryBase { + private readonly Type _registerType; + private readonly Type _registerImplementation; + + public MultiInstanceFactory(Type registerType, Type registerImplementation) { + if(registerImplementation.IsAbstract() || registerImplementation.IsInterface()) { + throw new DependencyContainerRegistrationException(registerImplementation, + "MultiInstanceFactory", + true); + } + + if(!DependencyContainer.IsValidAssignment(registerType, registerImplementation)) { + throw new DependencyContainerRegistrationException(registerImplementation, + "MultiInstanceFactory", + true); + } + + this._registerType = registerType; + this._registerImplementation = registerImplementation; + } + + public override Type CreatesType => this._registerImplementation; + + public override ObjectFactoryBase SingletonVariant => + new SingletonFactory(this._registerType, this._registerImplementation); + + public override ObjectFactoryBase MultiInstanceVariant => this; + + public override Object GetObject( + Type requestedType, + DependencyContainer container, + DependencyContainerResolveOptions options) { + try { + return container.RegisteredTypes.ConstructType(this._registerImplementation, this.Constructor, options); + } catch(DependencyContainerResolutionException ex) { + throw new DependencyContainerResolutionException(this._registerType, ex); + } + } + } + + /// + /// + /// IObjectFactory that invokes a specified delegate to construct the object. + /// + internal class DelegateFactory : ObjectFactoryBase { + private readonly Type _registerType; + + private readonly Func, Object> _factory; + + public DelegateFactory( + Type registerType, + Func, Object> factory) { + this._factory = factory ?? throw new ArgumentNullException(nameof(factory)); + + this._registerType = registerType; + } + + public override Boolean AssumeConstruction => true; + + public override Type CreatesType => this._registerType; + + public override ObjectFactoryBase WeakReferenceVariant => new WeakDelegateFactory(this._registerType, this._factory); + + public override ObjectFactoryBase StrongReferenceVariant => this; + + public override Object GetObject( + Type requestedType, + DependencyContainer container, + DependencyContainerResolveOptions options) { + try { + return this._factory.Invoke(container, options.ConstructorParameters); + } catch(Exception ex) { + throw new DependencyContainerResolutionException(this._registerType, ex); + } + } + } + + /// + /// + /// IObjectFactory that invokes a specified delegate to construct the object + /// Holds the delegate using a weak reference. + /// + internal class WeakDelegateFactory : ObjectFactoryBase { + private readonly Type _registerType; + + private readonly WeakReference _factory; + + public WeakDelegateFactory(Type registerType, + Func, Object> factory) { + if(factory == null) { + throw new ArgumentNullException(nameof(factory)); + } + + this._factory = new WeakReference(factory); + + this._registerType = registerType; + } + + public override Boolean AssumeConstruction => true; + + public override Type CreatesType => this._registerType; + + public override ObjectFactoryBase StrongReferenceVariant { + get { + if(!(this._factory.Target is Func, global::System.Object> factory)) { + throw new DependencyContainerWeakReferenceException(this._registerType); + } + + return new DelegateFactory(this._registerType, factory); + } + } + + public override ObjectFactoryBase WeakReferenceVariant => this; + + public override Object GetObject( + Type requestedType, + DependencyContainer container, + DependencyContainerResolveOptions options) { + if(!(this._factory.Target is Func, global::System.Object> factory)) { + throw new DependencyContainerWeakReferenceException(this._registerType); + } + + try { + return factory.Invoke(container, options.ConstructorParameters); + } catch(Exception ex) { + throw new DependencyContainerResolutionException(this._registerType, ex); + } + } + } + + /// + /// Stores an particular instance to return for a type. + /// + internal class InstanceFactory : ObjectFactoryBase, IDisposable { + private readonly Type _registerType; + private readonly Type _registerImplementation; + private readonly Object _instance; + + public InstanceFactory(Type registerType, Type registerImplementation, Object instance) { + if(!DependencyContainer.IsValidAssignment(registerType, registerImplementation)) { + throw new DependencyContainerRegistrationException(registerImplementation, "InstanceFactory", true); + } + + this._registerType = registerType; + this._registerImplementation = registerImplementation; + this._instance = instance; + } + + public override Boolean AssumeConstruction => true; + + public override Type CreatesType => this._registerImplementation; + + public override ObjectFactoryBase MultiInstanceVariant => + new MultiInstanceFactory(this._registerType, this._registerImplementation); + + public override ObjectFactoryBase WeakReferenceVariant => + new WeakInstanceFactory(this._registerType, this._registerImplementation, this._instance); + + public override ObjectFactoryBase StrongReferenceVariant => this; + + public override Object GetObject( + Type requestedType, + DependencyContainer container, + DependencyContainerResolveOptions options) => this._instance; + + public void Dispose() { + IDisposable disposable = this._instance as IDisposable; + + disposable?.Dispose(); + } + } + + /// + /// Stores the instance with a weak reference. + /// + internal class WeakInstanceFactory : ObjectFactoryBase, IDisposable { + private readonly Type _registerType; + private readonly Type _registerImplementation; + private readonly WeakReference _instance; + + public WeakInstanceFactory(Type registerType, Type registerImplementation, Object instance) { + if(!DependencyContainer.IsValidAssignment(registerType, registerImplementation)) { + throw new DependencyContainerRegistrationException( + registerImplementation, + "WeakInstanceFactory", + true); + } + + this._registerType = registerType; + this._registerImplementation = registerImplementation; + this._instance = new WeakReference(instance); + } + + public override Type CreatesType => this._registerImplementation; + + public override ObjectFactoryBase MultiInstanceVariant => + new MultiInstanceFactory(this._registerType, this._registerImplementation); + + public override ObjectFactoryBase WeakReferenceVariant => this; + + public override ObjectFactoryBase StrongReferenceVariant { + get { + Object instance = this._instance.Target; + + if(instance == null) { + throw new DependencyContainerWeakReferenceException(this._registerType); + } + + return new InstanceFactory(this._registerType, this._registerImplementation, instance); + } + } + + public override Object GetObject( + Type requestedType, + DependencyContainer container, + DependencyContainerResolveOptions options) { + Object instance = this._instance.Target; + + if(instance == null) { + throw new DependencyContainerWeakReferenceException(this._registerType); + } + + return instance; + } + + public void Dispose() => (this._instance.Target as IDisposable)?.Dispose(); + } + + /// + /// A factory that lazy instantiates a type and always returns the same instance. + /// + internal class SingletonFactory : ObjectFactoryBase, IDisposable { + private readonly Type _registerType; + private readonly Type _registerImplementation; + private readonly Object _singletonLock = new Object(); + private Object _current; + + public SingletonFactory(Type registerType, Type registerImplementation) { + if(registerImplementation.IsAbstract() || registerImplementation.IsInterface()) { + throw new DependencyContainerRegistrationException(registerImplementation, nameof(SingletonFactory), true); + } + + if(!DependencyContainer.IsValidAssignment(registerType, registerImplementation)) { + throw new DependencyContainerRegistrationException(registerImplementation, nameof(SingletonFactory), true); + } + + this._registerType = registerType; + this._registerImplementation = registerImplementation; + } + + public override Type CreatesType => this._registerImplementation; + + public override ObjectFactoryBase SingletonVariant => this; + + public override ObjectFactoryBase MultiInstanceVariant => + new MultiInstanceFactory(this._registerType, this._registerImplementation); + + public override Object GetObject( + Type requestedType, + DependencyContainer container, + DependencyContainerResolveOptions options) { + if(options.ConstructorParameters.Count != 0) { + throw new ArgumentException("Cannot specify parameters for singleton types"); + } + + lock(this._singletonLock) { + if(this._current == null) { + this._current = container.RegisteredTypes.ConstructType(this._registerImplementation, this.Constructor, options); + } + } + + return this._current; + } + + public override ObjectFactoryBase GetFactoryForChildContainer( + Type type, + DependencyContainer parent, + DependencyContainer child) { + // We make sure that the singleton is constructed before the child container takes the factory. + // Otherwise the results would vary depending on whether or not the parent container had resolved + // the type before the child container does. + _ = this.GetObject(type, parent, DependencyContainerResolveOptions.Default); + return this; + } + + public void Dispose() => (this._current as IDisposable)?.Dispose(); + } } diff --git a/Unosquare.Swan/Components/ProcessResult.cs b/Unosquare.Swan/Components/ProcessResult.cs index f5c9e64..a07faaa 100644 --- a/Unosquare.Swan/Components/ProcessResult.cs +++ b/Unosquare.Swan/Components/ProcessResult.cs @@ -1,46 +1,51 @@ -namespace Unosquare.Swan.Components -{ +using System; + +namespace Unosquare.Swan.Components { + /// + /// Represents the text of the standard output and standard error + /// of a process, including its exit code. + /// + public class ProcessResult { /// - /// Represents the text of the standard output and standard error - /// of a process, including its exit code. + /// Initializes a new instance of the class. /// - public class ProcessResult - { - /// - /// Initializes a new instance of the class. - /// - /// The exit code. - /// The standard output. - /// The standard error. - public ProcessResult(int exitCode, string standardOutput, string standardError) - { - ExitCode = exitCode; - StandardOutput = standardOutput; - StandardError = standardError; - } - - /// - /// Gets the exit code. - /// - /// - /// The exit code. - /// - public int ExitCode { get; } - - /// - /// Gets the text of the standard output. - /// - /// - /// The standard output. - /// - public string StandardOutput { get; } - - /// - /// Gets the text of the standard error. - /// - /// - /// The standard error. - /// - public string StandardError { get; } - } + /// The exit code. + /// The standard output. + /// The standard error. + public ProcessResult(Int32 exitCode, String standardOutput, String standardError) { + this.ExitCode = exitCode; + this.StandardOutput = standardOutput; + this.StandardError = standardError; + } + + /// + /// Gets the exit code. + /// + /// + /// The exit code. + /// + public Int32 ExitCode { + get; + } + + /// + /// Gets the text of the standard output. + /// + /// + /// The standard output. + /// + public String StandardOutput { + get; + } + + /// + /// Gets the text of the standard error. + /// + /// + /// The standard error. + /// + public String StandardError { + get; + } + } } \ No newline at end of file diff --git a/Unosquare.Swan/Components/ProcessRunner.cs b/Unosquare.Swan/Components/ProcessRunner.cs index 1a7d33e..bafeade 100644 --- a/Unosquare.Swan/Components/ProcessRunner.cs +++ b/Unosquare.Swan/Components/ProcessRunner.cs @@ -1,474 +1,447 @@ -namespace Unosquare.Swan.Components -{ - using System; - using System.Diagnostics; - using System.IO; - using System.Linq; - using System.Text; - using System.Threading; - using System.Threading.Tasks; - +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Unosquare.Swan.Components { + /// + /// Provides methods to help create external processes, and efficiently capture the + /// standard error and standard output streams. + /// + public static class ProcessRunner { /// - /// Provides methods to help create external processes, and efficiently capture the - /// standard error and standard output streams. + /// Defines a delegate to handle binary data reception from the standard + /// output or standard error streams from a process. /// - public static class ProcessRunner - { - /// - /// Defines a delegate to handle binary data reception from the standard - /// output or standard error streams from a process. - /// - /// The process data. - /// The process. - public delegate void ProcessDataReceivedCallback(byte[] processData, Process process); - - /// - /// Runs the process asynchronously and if the exit code is 0, - /// returns all of the standard output text. If the exit code is something other than 0 - /// it returns the contents of standard error. - /// This method is meant to be used for programs that output a relatively small amount of text. - /// - /// The filename. - /// The cancellation token. - /// The type of the result produced by this Task. - public static Task GetProcessOutputAsync(string filename, CancellationToken ct = default) => - GetProcessOutputAsync(filename, string.Empty, ct); - - /// - /// Runs the process asynchronously and if the exit code is 0, - /// returns all of the standard output text. If the exit code is something other than 0 - /// it returns the contents of standard error. - /// This method is meant to be used for programs that output a relatively small amount of text. - /// - /// The filename. - /// The arguments. - /// The cancellation token. - /// The type of the result produced by this Task. - /// - /// The following code explains how to run an external process using the - /// method. - /// - /// class Example - /// { - /// using System.Threading.Tasks; - /// using Unosquare.Swan.Components; - /// - /// static async Task Main() - /// { - /// // execute a process and save its output - /// var data = await ProcessRunner. - /// GetProcessOutputAsync("dotnet", "--help"); - /// - /// // print the output - /// data.WriteLine(); - /// } - /// } - /// - /// - public static async Task GetProcessOutputAsync( - string filename, - string arguments, - CancellationToken ct = default) - { - var result = await GetProcessResultAsync(filename, arguments, ct).ConfigureAwait(false); - return result.ExitCode == 0 ? result.StandardOutput : result.StandardError; - } - - /// - /// Gets the process output asynchronous. - /// - /// The filename. - /// The arguments. - /// The working directory. - /// The cancellation token. - /// - /// The type of the result produced by this Task. - /// - public static async Task GetProcessOutputAsync( - string filename, - string arguments, - string workingDirectory, - CancellationToken ct = default) - { - var result = await GetProcessResultAsync(filename, arguments, workingDirectory, ct: ct).ConfigureAwait(false); - return result.ExitCode == 0 ? result.StandardOutput : result.StandardError; - } - - /// - /// Runs the process asynchronously and if the exit code is 0, - /// returns all of the standard output text. If the exit code is something other than 0 - /// it returns the contents of standard error. - /// This method is meant to be used for programs that output a relatively small amount - /// of text using a different encoder. - /// - /// The filename. - /// The arguments. - /// The encoding. - /// The cancellation token. - /// - /// The type of the result produced by this Task. - /// - public static async Task GetProcessEncodedOutputAsync( - string filename, - string arguments = "", - Encoding encoding = null, - CancellationToken ct = default) - { - var result = await GetProcessResultAsync(filename, arguments, null, encoding, ct).ConfigureAwait(false); - return result.ExitCode == 0 ? result.StandardOutput : result.StandardError; - } - - /// - /// Executes a process asynchronously and returns the text of the standard output and standard error streams - /// along with the exit code. This method is meant to be used for programs that output a relatively small - /// amount of text. - /// - /// The filename. - /// The arguments. - /// The cancellation token. - /// - /// Text of the standard output and standard error streams along with the exit code as a instance. - /// - /// filename. - public static Task GetProcessResultAsync( - string filename, - string arguments = "", - CancellationToken ct = default) => - GetProcessResultAsync(filename, arguments, null, Definitions.CurrentAnsiEncoding, ct); - - /// - /// Executes a process asynchronously and returns the text of the standard output and standard error streams - /// along with the exit code. This method is meant to be used for programs that output a relatively small - /// amount of text. - /// - /// The filename. - /// The arguments. - /// The working directory. - /// The encoding. - /// The cancellation token. - /// - /// Text of the standard output and standard error streams along with the exit code as a instance. - /// - /// filename. - /// - /// The following code describes how to run an external process using the method. - /// - /// class Example - /// { - /// using System.Threading.Tasks; - /// using Unosquare.Swan.Components; - /// static async Task Main() - /// { - /// // Execute a process asynchronously - /// var data = await ProcessRunner.GetProcessResultAsync("dotnet", "--help"); - /// // print out the exit code - /// $"{data.ExitCode}".WriteLine(); - /// // print out the output - /// data.StandardOutput.WriteLine(); - /// // and the error if exists - /// data.StandardError.Error(); - /// } - /// } - /// - public static async Task GetProcessResultAsync( - string filename, - string arguments, - string workingDirectory, - Encoding encoding = null, - CancellationToken ct = default) - { - if (filename == null) - throw new ArgumentNullException(nameof(filename)); - - if (encoding == null) - encoding = Definitions.CurrentAnsiEncoding; - - var standardOutputBuilder = new StringBuilder(); - var standardErrorBuilder = new StringBuilder(); - - var processReturn = await RunProcessAsync( - filename, - arguments, - workingDirectory, - (data, proc) => { standardOutputBuilder.Append(encoding.GetString(data)); }, - (data, proc) => { standardErrorBuilder.Append(encoding.GetString(data)); }, - encoding, - true, - ct) - .ConfigureAwait(false); - - return new ProcessResult(processReturn, standardOutputBuilder.ToString(), standardErrorBuilder.ToString()); - } - - /// - /// Runs an external process asynchronously, providing callbacks to - /// capture binary data from the standard error and standard output streams. - /// The callbacks contain a reference to the process so you can respond to output or - /// error streams by writing to the process' input stream. - /// The exit code (return value) will be -1 for forceful termination of the process. - /// - /// The filename. - /// The arguments. - /// The working directory. - /// The on output data. - /// The on error data. - /// The encoding. - /// if set to true the next data callback will wait until the current one completes. - /// The cancellation token. - /// - /// Value type will be -1 for forceful termination of the process. - /// - public static Task RunProcessAsync( - string filename, - string arguments, - string workingDirectory, - ProcessDataReceivedCallback onOutputData, - ProcessDataReceivedCallback onErrorData, - Encoding encoding, - bool syncEvents = true, - CancellationToken ct = default) - { - if (filename == null) - throw new ArgumentNullException(nameof(filename)); - - return Task.Run(() => - { - // Setup the process and its corresponding start info - var process = new Process - { - EnableRaisingEvents = false, - StartInfo = new ProcessStartInfo - { - Arguments = arguments, - CreateNoWindow = true, - FileName = filename, - RedirectStandardError = true, - StandardErrorEncoding = encoding, - RedirectStandardOutput = true, - StandardOutputEncoding = encoding, - UseShellExecute = false, + /// The process data. + /// The process. + public delegate void ProcessDataReceivedCallback(Byte[] processData, Process process); + + /// + /// Runs the process asynchronously and if the exit code is 0, + /// returns all of the standard output text. If the exit code is something other than 0 + /// it returns the contents of standard error. + /// This method is meant to be used for programs that output a relatively small amount of text. + /// + /// The filename. + /// The cancellation token. + /// The type of the result produced by this Task. + public static Task GetProcessOutputAsync(String filename, CancellationToken ct = default) => + GetProcessOutputAsync(filename, String.Empty, ct); + + /// + /// Runs the process asynchronously and if the exit code is 0, + /// returns all of the standard output text. If the exit code is something other than 0 + /// it returns the contents of standard error. + /// This method is meant to be used for programs that output a relatively small amount of text. + /// + /// The filename. + /// The arguments. + /// The cancellation token. + /// The type of the result produced by this Task. + /// + /// The following code explains how to run an external process using the + /// method. + /// + /// class Example + /// { + /// using System.Threading.Tasks; + /// using Unosquare.Swan.Components; + /// + /// static async Task Main() + /// { + /// // execute a process and save its output + /// var data = await ProcessRunner. + /// GetProcessOutputAsync("dotnet", "--help"); + /// + /// // print the output + /// data.WriteLine(); + /// } + /// } + /// + /// + public static async Task GetProcessOutputAsync( + String filename, + String arguments, + CancellationToken ct = default) { + ProcessResult result = await GetProcessResultAsync(filename, arguments, ct).ConfigureAwait(false); + return result.ExitCode == 0 ? result.StandardOutput : result.StandardError; + } + + /// + /// Gets the process output asynchronous. + /// + /// The filename. + /// The arguments. + /// The working directory. + /// The cancellation token. + /// + /// The type of the result produced by this Task. + /// + public static async Task GetProcessOutputAsync( + String filename, + String arguments, + String workingDirectory, + CancellationToken ct = default) { + ProcessResult result = await GetProcessResultAsync(filename, arguments, workingDirectory, ct: ct).ConfigureAwait(false); + return result.ExitCode == 0 ? result.StandardOutput : result.StandardError; + } + + /// + /// Runs the process asynchronously and if the exit code is 0, + /// returns all of the standard output text. If the exit code is something other than 0 + /// it returns the contents of standard error. + /// This method is meant to be used for programs that output a relatively small amount + /// of text using a different encoder. + /// + /// The filename. + /// The arguments. + /// The encoding. + /// The cancellation token. + /// + /// The type of the result produced by this Task. + /// + public static async Task GetProcessEncodedOutputAsync( + String filename, + String arguments = "", + Encoding encoding = null, + CancellationToken ct = default) { + ProcessResult result = await GetProcessResultAsync(filename, arguments, null, encoding, ct).ConfigureAwait(false); + return result.ExitCode == 0 ? result.StandardOutput : result.StandardError; + } + + /// + /// Executes a process asynchronously and returns the text of the standard output and standard error streams + /// along with the exit code. This method is meant to be used for programs that output a relatively small + /// amount of text. + /// + /// The filename. + /// The arguments. + /// The cancellation token. + /// + /// Text of the standard output and standard error streams along with the exit code as a instance. + /// + /// filename. + public static Task GetProcessResultAsync( + String filename, + String arguments = "", + CancellationToken ct = default) => + GetProcessResultAsync(filename, arguments, null, Definitions.CurrentAnsiEncoding, ct); + + /// + /// Executes a process asynchronously and returns the text of the standard output and standard error streams + /// along with the exit code. This method is meant to be used for programs that output a relatively small + /// amount of text. + /// + /// The filename. + /// The arguments. + /// The working directory. + /// The encoding. + /// The cancellation token. + /// + /// Text of the standard output and standard error streams along with the exit code as a instance. + /// + /// filename. + /// + /// The following code describes how to run an external process using the method. + /// + /// class Example + /// { + /// using System.Threading.Tasks; + /// using Unosquare.Swan.Components; + /// static async Task Main() + /// { + /// // Execute a process asynchronously + /// var data = await ProcessRunner.GetProcessResultAsync("dotnet", "--help"); + /// // print out the exit code + /// $"{data.ExitCode}".WriteLine(); + /// // print out the output + /// data.StandardOutput.WriteLine(); + /// // and the error if exists + /// data.StandardError.Error(); + /// } + /// } + /// + public static async Task GetProcessResultAsync( + String filename, + String arguments, + String workingDirectory, + Encoding encoding = null, + CancellationToken ct = default) { + if(filename == null) { + throw new ArgumentNullException(nameof(filename)); + } + + if(encoding == null) { + encoding = Definitions.CurrentAnsiEncoding; + } + + StringBuilder standardOutputBuilder = new StringBuilder(); + StringBuilder standardErrorBuilder = new StringBuilder(); + + Int32 processReturn = await RunProcessAsync( + filename, + arguments, + workingDirectory, + (data, proc) => standardOutputBuilder.Append(encoding.GetString(data)), + (data, proc) => standardErrorBuilder.Append(encoding.GetString(data)), + encoding, + true, + ct) + .ConfigureAwait(false); + + return new ProcessResult(processReturn, standardOutputBuilder.ToString(), standardErrorBuilder.ToString()); + } + + /// + /// Runs an external process asynchronously, providing callbacks to + /// capture binary data from the standard error and standard output streams. + /// The callbacks contain a reference to the process so you can respond to output or + /// error streams by writing to the process' input stream. + /// The exit code (return value) will be -1 for forceful termination of the process. + /// + /// The filename. + /// The arguments. + /// The working directory. + /// The on output data. + /// The on error data. + /// The encoding. + /// if set to true the next data callback will wait until the current one completes. + /// The cancellation token. + /// + /// Value type will be -1 for forceful termination of the process. + /// + public static Task RunProcessAsync( + String filename, + String arguments, + String workingDirectory, + ProcessDataReceivedCallback onOutputData, + ProcessDataReceivedCallback onErrorData, + Encoding encoding, + Boolean syncEvents = true, + CancellationToken ct = default) { + if(filename == null) { + throw new ArgumentNullException(nameof(filename)); + } + + return Task.Run(() => { + // Setup the process and its corresponding start info + Process process = new Process { + EnableRaisingEvents = false, + StartInfo = new ProcessStartInfo { + Arguments = arguments, + CreateNoWindow = true, + FileName = filename, + RedirectStandardError = true, + StandardErrorEncoding = encoding, + RedirectStandardOutput = true, + StandardOutputEncoding = encoding, + UseShellExecute = false, #if NET452 - WindowStyle = ProcessWindowStyle.Hidden, + WindowStyle = ProcessWindowStyle.Hidden, #endif - }, - }; - - if (!string.IsNullOrWhiteSpace(workingDirectory)) - process.StartInfo.WorkingDirectory = workingDirectory; - - // Launch the process and discard any buffered data for standard error and standard output - process.Start(); - process.StandardError.DiscardBufferedData(); - process.StandardOutput.DiscardBufferedData(); - - // Launch the asynchronous stream reading tasks - var readTasks = new Task[2]; - readTasks[0] = CopyStreamAsync( - process, - process.StandardOutput.BaseStream, - onOutputData, - syncEvents, - ct); - readTasks[1] = CopyStreamAsync( - process, - process.StandardError.BaseStream, - onErrorData, - syncEvents, - ct); - - try - { - // Wait for all tasks to complete - Task.WaitAll(readTasks, ct); - } - catch (TaskCanceledException) - { - // ignore - } - finally - { - // Wait for the process to exit - while (ct.IsCancellationRequested == false) - { - if (process.HasExited || process.WaitForExit(5)) - break; - } - - // Forcefully kill the process if it do not exit - try - { - if (process.HasExited == false) - process.Kill(); - } - catch - { - // swallow - } - } - - try - { - // Retrieve and return the exit code. - // -1 signals error - return process.HasExited ? process.ExitCode : -1; - } - catch - { - return -1; - } - }, ct); - } - - /// - /// Runs an external process asynchronously, providing callbacks to - /// capture binary data from the standard error and standard output streams. - /// The callbacks contain a reference to the process so you can respond to output or - /// error streams by writing to the process' input stream. - /// The exit code (return value) will be -1 for forceful termination of the process. - /// - /// The filename. - /// The arguments. - /// The on output data. - /// The on error data. - /// if set to true the next data callback will wait until the current one completes. - /// The cancellation token. - /// Value type will be -1 for forceful termination of the process. - /// - /// The following example illustrates how to run an external process using the - /// - /// method. - /// - /// class Example - /// { - /// using System.Diagnostics; - /// using System.Text; - /// using System.Threading.Tasks; - /// using Unosquare.Swan; - /// using Unosquare.Swan.Components; - /// - /// static async Task Main() - /// { - /// // Execute a process asynchronously - /// var data = await ProcessRunner - /// .RunProcessAsync("dotnet", "--help", Print, Print); - /// - /// // flush all messages - /// Terminal.Flush(); - /// } - /// - /// // a callback to print both output or errors - /// static void Print(byte[] data, Process proc) => - /// Encoding.GetEncoding(0).GetString(data).WriteLine(); - /// } - /// - /// - public static Task RunProcessAsync( - string filename, - string arguments, - ProcessDataReceivedCallback onOutputData, - ProcessDataReceivedCallback onErrorData, - bool syncEvents = true, - CancellationToken ct = default) - => RunProcessAsync( - filename, - arguments, - null, - onOutputData, - onErrorData, - Definitions.CurrentAnsiEncoding, - syncEvents, - ct); - - /// - /// Copies the stream asynchronously. - /// - /// The process. - /// The source stream. - /// The on data callback. - /// if set to true [synchronize events]. - /// The cancellation token. - /// Total copies stream. - private static Task CopyStreamAsync( - Process process, - Stream baseStream, - ProcessDataReceivedCallback onDataCallback, - bool syncEvents, - CancellationToken ct) - { - return Task.Factory.StartNew(async () => - { - // define some state variables - var swapBuffer = new byte[2048]; // the buffer to copy data from one stream to the next - ulong totalCount = 0; // the total amount of bytes read - var hasExited = false; - - while (ct.IsCancellationRequested == false) - { - try - { - // Check if process is no longer valid - // if this condition holds, simply read the last bits of data available. - int readCount; // the bytes read in any given event - if (process.HasExited || process.WaitForExit(1)) - { - while (true) - { - try - { - readCount = await baseStream.ReadAsync(swapBuffer, 0, swapBuffer.Length, ct); - - if (readCount > 0) - { - totalCount += (ulong) readCount; - onDataCallback?.Invoke(swapBuffer.Skip(0).Take(readCount).ToArray(), process); - } - else - { - hasExited = true; - break; - } - } - catch - { - hasExited = true; - break; - } - } - } - - if (hasExited) break; - - // Try reading from the stream. < 0 means no read occurred. - readCount = await baseStream.ReadAsync(swapBuffer, 0, swapBuffer.Length, ct); - - // When no read is done, we need to let is rest for a bit - if (readCount <= 0) - { - await Task.Delay(1, ct); // do not hog CPU cycles doing nothing. - continue; - } - - totalCount += (ulong) readCount; - if (onDataCallback == null) continue; - - // Create the buffer to pass to the callback - var eventBuffer = swapBuffer.Skip(0).Take(readCount).ToArray(); - - // Create the data processing callback invocation - var eventTask = - Task.Factory.StartNew(() => { onDataCallback.Invoke(eventBuffer, process); }, ct); - - // wait for the event to process before the next read occurs - if (syncEvents) eventTask.Wait(ct); - } - catch - { - break; - } - } - - return totalCount; - }, ct).Unwrap(); - } - } + }, + }; + + if(!String.IsNullOrWhiteSpace(workingDirectory)) { + process.StartInfo.WorkingDirectory = workingDirectory; + } + + // Launch the process and discard any buffered data for standard error and standard output + process.Start(); + process.StandardError.DiscardBufferedData(); + process.StandardOutput.DiscardBufferedData(); + + // Launch the asynchronous stream reading tasks + Task[] readTasks = new Task[2]; + readTasks[0] = CopyStreamAsync( + process, + process.StandardOutput.BaseStream, + onOutputData, + syncEvents, + ct); + readTasks[1] = CopyStreamAsync( + process, + process.StandardError.BaseStream, + onErrorData, + syncEvents, + ct); + + try { + // Wait for all tasks to complete + Task.WaitAll(readTasks, ct); + } catch(TaskCanceledException) { + // ignore + } finally { + // Wait for the process to exit + while(ct.IsCancellationRequested == false) { + if(process.HasExited || process.WaitForExit(5)) { + break; + } + } + + // Forcefully kill the process if it do not exit + try { + if(process.HasExited == false) { + process.Kill(); + } + } catch { + // swallow + } + } + + try { + // Retrieve and return the exit code. + // -1 signals error + return process.HasExited ? process.ExitCode : -1; + } catch { + return -1; + } + }, ct); + } + + /// + /// Runs an external process asynchronously, providing callbacks to + /// capture binary data from the standard error and standard output streams. + /// The callbacks contain a reference to the process so you can respond to output or + /// error streams by writing to the process' input stream. + /// The exit code (return value) will be -1 for forceful termination of the process. + /// + /// The filename. + /// The arguments. + /// The on output data. + /// The on error data. + /// if set to true the next data callback will wait until the current one completes. + /// The cancellation token. + /// Value type will be -1 for forceful termination of the process. + /// + /// The following example illustrates how to run an external process using the + /// + /// method. + /// + /// class Example + /// { + /// using System.Diagnostics; + /// using System.Text; + /// using System.Threading.Tasks; + /// using Unosquare.Swan; + /// using Unosquare.Swan.Components; + /// + /// static async Task Main() + /// { + /// // Execute a process asynchronously + /// var data = await ProcessRunner + /// .RunProcessAsync("dotnet", "--help", Print, Print); + /// + /// // flush all messages + /// Terminal.Flush(); + /// } + /// + /// // a callback to print both output or errors + /// static void Print(byte[] data, Process proc) => + /// Encoding.GetEncoding(0).GetString(data).WriteLine(); + /// } + /// + /// + public static Task RunProcessAsync( + String filename, + String arguments, + ProcessDataReceivedCallback onOutputData, + ProcessDataReceivedCallback onErrorData, + Boolean syncEvents = true, + CancellationToken ct = default) + => RunProcessAsync( + filename, + arguments, + null, + onOutputData, + onErrorData, + Definitions.CurrentAnsiEncoding, + syncEvents, + ct); + + /// + /// Copies the stream asynchronously. + /// + /// The process. + /// The source stream. + /// The on data callback. + /// if set to true [synchronize events]. + /// The cancellation token. + /// Total copies stream. + private static Task CopyStreamAsync( + Process process, + Stream baseStream, + ProcessDataReceivedCallback onDataCallback, + Boolean syncEvents, + CancellationToken ct) => Task.Factory.StartNew(async () => { + // define some state variables + Byte[] swapBuffer = new Byte[2048]; // the buffer to copy data from one stream to the next + UInt64 totalCount = 0; // the total amount of bytes read + Boolean hasExited = false; + + while(ct.IsCancellationRequested == false) { + try { + // Check if process is no longer valid + // if this condition holds, simply read the last bits of data available. + Int32 readCount; // the bytes read in any given event + if(process.HasExited || process.WaitForExit(1)) { + while(true) { + try { + readCount = await baseStream.ReadAsync(swapBuffer, 0, swapBuffer.Length, ct); + + if(readCount > 0) { + totalCount += (UInt64)readCount; + onDataCallback?.Invoke(swapBuffer.Skip(0).Take(readCount).ToArray(), process); + } else { + hasExited = true; + break; + } + } catch { + hasExited = true; + break; + } + } + } + + if(hasExited) { + break; + } + + // Try reading from the stream. < 0 means no read occurred. + readCount = await baseStream.ReadAsync(swapBuffer, 0, swapBuffer.Length, ct); + + // When no read is done, we need to let is rest for a bit + if(readCount <= 0) { + await Task.Delay(1, ct); // do not hog CPU cycles doing nothing. + continue; + } + + totalCount += (UInt64)readCount; + if(onDataCallback == null) { + continue; + } + + // Create the buffer to pass to the callback + Byte[] eventBuffer = swapBuffer.Skip(0).Take(readCount).ToArray(); + + // Create the data processing callback invocation + Task eventTask = + Task.Factory.StartNew(() => onDataCallback.Invoke(eventBuffer, process), ct); + + // wait for the event to process before the next read occurs + if(syncEvents) { + eventTask.Wait(ct); + } + } catch { + break; + } + } + + return totalCount; + }, ct).Unwrap(); + } } \ No newline at end of file diff --git a/Unosquare.Swan/Components/RealtimeClock.cs b/Unosquare.Swan/Components/RealtimeClock.cs index 894934c..fd5443a 100644 --- a/Unosquare.Swan/Components/RealtimeClock.cs +++ b/Unosquare.Swan/Components/RealtimeClock.cs @@ -1,143 +1,128 @@ -namespace Unosquare.Swan.Components -{ - using System; - using System.Diagnostics; - using Abstractions; - +using System; +using System.Diagnostics; +using Unosquare.Swan.Abstractions; + +namespace Unosquare.Swan.Components { + /// + /// A time measurement artifact. + /// + internal sealed class RealTimeClock : IDisposable { + private readonly Stopwatch _chrono = new Stopwatch(); + private ISyncLocker _locker = SyncLockerFactory.Create(useSlim: true); + private Int64 _offsetTicks; + private Double _speedRatio = 1.0d; + private Boolean _isDisposed; + /// - /// A time measurement artifact. + /// Initializes a new instance of the class. + /// The clock starts paused and at the 0 position. /// - internal sealed class RealTimeClock : IDisposable - { - private readonly Stopwatch _chrono = new Stopwatch(); - private ISyncLocker _locker = SyncLockerFactory.Create(useSlim: true); - private long _offsetTicks; - private double _speedRatio = 1.0d; - private bool _isDisposed; - - /// - /// Initializes a new instance of the class. - /// The clock starts paused and at the 0 position. - /// - public RealTimeClock() - { - Reset(); - } - - /// - /// Gets or sets the clock position. - /// - public TimeSpan Position - { - get - { - using (_locker.AcquireReaderLock()) - { - return TimeSpan.FromTicks( - _offsetTicks + Convert.ToInt64(_chrono.Elapsed.Ticks * SpeedRatio)); - } - } - } - - /// - /// Gets a value indicating whether the clock is running. - /// - public bool IsRunning - { - get - { - using (_locker.AcquireReaderLock()) - { - return _chrono.IsRunning; - } - } - } - - /// - /// Gets or sets the speed ratio at which the clock runs. - /// - public double SpeedRatio - { - get - { - using (_locker.AcquireReaderLock()) - { - return _speedRatio; - } - } - set - { - using (_locker.AcquireWriterLock()) - { - if (value < 0d) value = 0d; - - // Capture the initial position se we set it even after the speedratio has changed - // this ensures a smooth position transition - var initialPosition = Position; - _speedRatio = value; - Update(initialPosition); - } - } - } - - /// - /// Sets a new position value atomically. - /// - /// The new value that the position porperty will hold. - public void Update(TimeSpan value) - { - using (_locker.AcquireWriterLock()) - { - var resume = _chrono.IsRunning; - _chrono.Reset(); - _offsetTicks = value.Ticks; - if (resume) _chrono.Start(); - } - } - - /// - /// Starts or resumes the clock. - /// - public void Play() - { - using (_locker.AcquireWriterLock()) - { - if (_chrono.IsRunning) return; - _chrono.Start(); - } - } - - /// - /// Pauses the clock. - /// - public void Pause() - { - using (_locker.AcquireWriterLock()) - { - _chrono.Stop(); - } - } - - /// - /// Sets the clock position to 0 and stops it. - /// The speed ratio is not modified. - /// - public void Reset() - { - using (_locker.AcquireWriterLock()) - { - _offsetTicks = 0; - _chrono.Reset(); - } - } - - /// - public void Dispose() - { - if (_isDisposed) return; - _isDisposed = true; - _locker?.Dispose(); - _locker = null; - } - } + public RealTimeClock() => this.Reset(); + + /// + /// Gets or sets the clock position. + /// + public TimeSpan Position { + get { + using(this._locker.AcquireReaderLock()) { + return TimeSpan.FromTicks( + this._offsetTicks + Convert.ToInt64(this._chrono.Elapsed.Ticks * this.SpeedRatio)); + } + } + } + + /// + /// Gets a value indicating whether the clock is running. + /// + public Boolean IsRunning { + get { + using(this._locker.AcquireReaderLock()) { + return this._chrono.IsRunning; + } + } + } + + /// + /// Gets or sets the speed ratio at which the clock runs. + /// + public Double SpeedRatio { + get { + using(this._locker.AcquireReaderLock()) { + return this._speedRatio; + } + } + set { + using(this._locker.AcquireWriterLock()) { + if(value < 0d) { + value = 0d; + } + + // Capture the initial position se we set it even after the speedratio has changed + // this ensures a smooth position transition + TimeSpan initialPosition = this.Position; + this._speedRatio = value; + this.Update(initialPosition); + } + } + } + + /// + /// Sets a new position value atomically. + /// + /// The new value that the position porperty will hold. + public void Update(TimeSpan value) { + using(this._locker.AcquireWriterLock()) { + Boolean resume = this._chrono.IsRunning; + this._chrono.Reset(); + this._offsetTicks = value.Ticks; + if(resume) { + this._chrono.Start(); + } + } + } + + /// + /// Starts or resumes the clock. + /// + public void Play() { + using(this._locker.AcquireWriterLock()) { + if(this._chrono.IsRunning) { + return; + } + + this._chrono.Start(); + } + } + + /// + /// Pauses the clock. + /// + public void Pause() { + using(this._locker.AcquireWriterLock()) { + this._chrono.Stop(); + } + } + + /// + /// Sets the clock position to 0 and stops it. + /// The speed ratio is not modified. + /// + public void Reset() { + using(this._locker.AcquireWriterLock()) { + this._offsetTicks = 0; + this._chrono.Reset(); + } + } + + /// + public void Dispose() { + if(this._isDisposed) { + return; + } + + this._isDisposed = true; + this._locker?.Dispose(); + this._locker = null; + } + } } \ No newline at end of file diff --git a/Unosquare.Swan/Components/RegisterOptions.cs b/Unosquare.Swan/Components/RegisterOptions.cs index aff98b5..2763126 100644 --- a/Unosquare.Swan/Components/RegisterOptions.cs +++ b/Unosquare.Swan/Components/RegisterOptions.cs @@ -1,132 +1,120 @@ -namespace Unosquare.Swan.Components -{ - using System; - using System.Collections.Generic; - using System.Linq; - using Exceptions; - +using System; +using System.Collections.Generic; +using System.Linq; +using Unosquare.Swan.Exceptions; + +namespace Unosquare.Swan.Components { + /// + /// Registration options for "fluent" API. + /// + public sealed class RegisterOptions { + private readonly TypesConcurrentDictionary _registeredTypes; + private readonly DependencyContainer.TypeRegistration _registration; + /// - /// Registration options for "fluent" API. + /// Initializes a new instance of the class. /// - public sealed class RegisterOptions - { - private readonly TypesConcurrentDictionary _registeredTypes; - private readonly DependencyContainer.TypeRegistration _registration; - - /// - /// Initializes a new instance of the class. - /// - /// The registered types. - /// The registration. - public RegisterOptions(TypesConcurrentDictionary registeredTypes, DependencyContainer.TypeRegistration registration) - { - _registeredTypes = registeredTypes; - _registration = registration; - } - - /// - /// Make registration a singleton (single instance) if possible. - /// - /// A registration options for fluent API. - /// Generic constraint registration exception. - public RegisterOptions AsSingleton() - { - var currentFactory = _registeredTypes.GetCurrentFactory(_registration); - - if (currentFactory == null) - throw new DependencyContainerRegistrationException(_registration.Type, "singleton"); - - return _registeredTypes.AddUpdateRegistration(_registration, currentFactory.SingletonVariant); - } - - /// - /// Make registration multi-instance if possible. - /// - /// A registration options for fluent API. - /// Generic constraint registration exception. - public RegisterOptions AsMultiInstance() - { - var currentFactory = _registeredTypes.GetCurrentFactory(_registration); - - if (currentFactory == null) - throw new DependencyContainerRegistrationException(_registration.Type, "multi-instance"); - - return _registeredTypes.AddUpdateRegistration(_registration, currentFactory.MultiInstanceVariant); - } - - /// - /// Make registration hold a weak reference if possible. - /// - /// A registration options for fluent API. - /// Generic constraint registration exception. - public RegisterOptions WithWeakReference() - { - var currentFactory = _registeredTypes.GetCurrentFactory(_registration); - - if (currentFactory == null) - throw new DependencyContainerRegistrationException(_registration.Type, "weak reference"); - - return _registeredTypes.AddUpdateRegistration(_registration, currentFactory.WeakReferenceVariant); - } - - /// - /// Make registration hold a strong reference if possible. - /// - /// A registration options for fluent API. - /// Generic constraint registration exception. - public RegisterOptions WithStrongReference() - { - var currentFactory = _registeredTypes.GetCurrentFactory(_registration); - - if (currentFactory == null) - throw new DependencyContainerRegistrationException(_registration.Type, "strong reference"); - - return _registeredTypes.AddUpdateRegistration(_registration, currentFactory.StrongReferenceVariant); - } - } - + /// The registered types. + /// The registration. + public RegisterOptions(TypesConcurrentDictionary registeredTypes, DependencyContainer.TypeRegistration registration) { + this._registeredTypes = registeredTypes; + this._registration = registration; + } + /// - /// Registration options for "fluent" API when registering multiple implementations. + /// Make registration a singleton (single instance) if possible. /// - public sealed class MultiRegisterOptions - { - private IEnumerable _registerOptions; - - /// - /// Initializes a new instance of the class. - /// - /// The register options. - public MultiRegisterOptions(IEnumerable registerOptions) - { - _registerOptions = registerOptions; - } - - /// - /// Make registration a singleton (single instance) if possible. - /// - /// A registration multi-instance for fluent API. - /// Generic Constraint Registration Exception. - public MultiRegisterOptions AsSingleton() - { - _registerOptions = ExecuteOnAllRegisterOptions(ro => ro.AsSingleton()); - return this; - } - - /// - /// Make registration multi-instance if possible. - /// - /// A registration multi-instance for fluent API. - /// Generic Constraint Registration Exception. - public MultiRegisterOptions AsMultiInstance() - { - _registerOptions = ExecuteOnAllRegisterOptions(ro => ro.AsMultiInstance()); - return this; - } - - private IEnumerable ExecuteOnAllRegisterOptions( - Func action) - { - return _registerOptions.Select(action).ToList(); - } - } + /// A registration options for fluent API. + /// Generic constraint registration exception. + public RegisterOptions AsSingleton() { + ObjectFactoryBase currentFactory = this._registeredTypes.GetCurrentFactory(this._registration); + + if(currentFactory == null) { + throw new DependencyContainerRegistrationException(this._registration.Type, "singleton"); + } + + return this._registeredTypes.AddUpdateRegistration(this._registration, currentFactory.SingletonVariant); + } + + /// + /// Make registration multi-instance if possible. + /// + /// A registration options for fluent API. + /// Generic constraint registration exception. + public RegisterOptions AsMultiInstance() { + ObjectFactoryBase currentFactory = this._registeredTypes.GetCurrentFactory(this._registration); + + if(currentFactory == null) { + throw new DependencyContainerRegistrationException(this._registration.Type, "multi-instance"); + } + + return this._registeredTypes.AddUpdateRegistration(this._registration, currentFactory.MultiInstanceVariant); + } + + /// + /// Make registration hold a weak reference if possible. + /// + /// A registration options for fluent API. + /// Generic constraint registration exception. + public RegisterOptions WithWeakReference() { + ObjectFactoryBase currentFactory = this._registeredTypes.GetCurrentFactory(this._registration); + + if(currentFactory == null) { + throw new DependencyContainerRegistrationException(this._registration.Type, "weak reference"); + } + + return this._registeredTypes.AddUpdateRegistration(this._registration, currentFactory.WeakReferenceVariant); + } + + /// + /// Make registration hold a strong reference if possible. + /// + /// A registration options for fluent API. + /// Generic constraint registration exception. + public RegisterOptions WithStrongReference() { + ObjectFactoryBase currentFactory = this._registeredTypes.GetCurrentFactory(this._registration); + + if(currentFactory == null) { + throw new DependencyContainerRegistrationException(this._registration.Type, "strong reference"); + } + + return this._registeredTypes.AddUpdateRegistration(this._registration, currentFactory.StrongReferenceVariant); + } + } + + /// + /// Registration options for "fluent" API when registering multiple implementations. + /// + public sealed class MultiRegisterOptions { + private IEnumerable _registerOptions; + + /// + /// Initializes a new instance of the class. + /// + /// The register options. + public MultiRegisterOptions(IEnumerable registerOptions) => this._registerOptions = registerOptions; + + /// + /// Make registration a singleton (single instance) if possible. + /// + /// A registration multi-instance for fluent API. + /// Generic Constraint Registration Exception. + public MultiRegisterOptions AsSingleton() { + this._registerOptions = this.ExecuteOnAllRegisterOptions(ro => ro.AsSingleton()); + return this; + } + + /// + /// Make registration multi-instance if possible. + /// + /// A registration multi-instance for fluent API. + /// Generic Constraint Registration Exception. + public MultiRegisterOptions AsMultiInstance() { + this._registerOptions = this.ExecuteOnAllRegisterOptions(ro => ro.AsMultiInstance()); + return this; + } + + private IEnumerable ExecuteOnAllRegisterOptions( + Func action) => this._registerOptions.Select(action).ToList(); + } } \ No newline at end of file diff --git a/Unosquare.Swan/Components/TypeRegistration.cs b/Unosquare.Swan/Components/TypeRegistration.cs index 0fa4dca..ab8db8c 100644 --- a/Unosquare.Swan/Components/TypeRegistration.cs +++ b/Unosquare.Swan/Components/TypeRegistration.cs @@ -1,67 +1,63 @@ -namespace Unosquare.Swan.Components -{ - using System; - - public partial class DependencyContainer - { - /// - /// Represents a Type Registration within the IoC Container. - /// - public sealed class TypeRegistration - { - private readonly int _hashCode; - - /// - /// Initializes a new instance of the class. - /// - /// The type. - /// The name. - public TypeRegistration(Type type, string name = null) - { - Type = type; - Name = name ?? string.Empty; - - _hashCode = string.Concat(Type.FullName, "|", Name).GetHashCode(); - } - - /// - /// Gets the type. - /// - /// - /// The type. - /// - public Type Type { get; } - - /// - /// Gets the name. - /// - /// - /// The name. - /// - public string Name { get; } - - /// - /// Determines whether the specified , is equal to this instance. - /// - /// The to compare with this instance. - /// - /// true if the specified is equal to this instance; otherwise, false. - /// - public override bool Equals(object obj) - { - if (!(obj is TypeRegistration typeRegistration) || typeRegistration.Type != Type) - return false; - - return string.Compare(Name, typeRegistration.Name, StringComparison.Ordinal) == 0; - } - - /// - /// Returns a hash code for this instance. - /// - /// - /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. - /// - public override int GetHashCode() => _hashCode; - } - } +using System; + +namespace Unosquare.Swan.Components { + public partial class DependencyContainer { + /// + /// Represents a Type Registration within the IoC Container. + /// + public sealed class TypeRegistration { + private readonly Int32 _hashCode; + + /// + /// Initializes a new instance of the class. + /// + /// The type. + /// The name. + public TypeRegistration(Type type, String name = null) { + this.Type = type; + this.Name = name ?? String.Empty; + + this._hashCode = String.Concat(this.Type.FullName, "|", this.Name).GetHashCode(); + } + + /// + /// Gets the type. + /// + /// + /// The type. + /// + public Type Type { + get; + } + + /// + /// Gets the name. + /// + /// + /// The name. + /// + public String Name { + get; + } + + /// + /// Determines whether the specified , is equal to this instance. + /// + /// The to compare with this instance. + /// + /// true if the specified is equal to this instance; otherwise, false. + /// + public override Boolean Equals(Object obj) => !(obj is TypeRegistration typeRegistration) || typeRegistration.Type != this.Type + ? false + : String.Compare(this.Name, typeRegistration.Name, StringComparison.Ordinal) == 0; + + /// + /// Returns a hash code for this instance. + /// + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// + public override Int32 GetHashCode() => this._hashCode; + } + } } \ No newline at end of file diff --git a/Unosquare.Swan/Components/TypesConcurrentDictionary.cs b/Unosquare.Swan/Components/TypesConcurrentDictionary.cs index 114c302..7e97527 100644 --- a/Unosquare.Swan/Components/TypesConcurrentDictionary.cs +++ b/Unosquare.Swan/Components/TypesConcurrentDictionary.cs @@ -1,352 +1,308 @@ -namespace Unosquare.Swan.Components -{ - using System; - using System.Linq.Expressions; - using System.Reflection; - using System.Collections.Generic; - using System.Linq; - using Exceptions; - using System.Collections.Concurrent; - +using System; +using System.Linq.Expressions; +using System.Reflection; +using System.Collections.Generic; +using System.Linq; +using Unosquare.Swan.Exceptions; +using System.Collections.Concurrent; + +namespace Unosquare.Swan.Components { + /// + /// Represents a Concurrent Dictionary for TypeRegistration. + /// + public class TypesConcurrentDictionary : ConcurrentDictionary { + private static readonly ConcurrentDictionary ObjectConstructorCache = + new ConcurrentDictionary(); + + private readonly DependencyContainer _dependencyContainer; + + internal TypesConcurrentDictionary(DependencyContainer dependencyContainer) => this._dependencyContainer = dependencyContainer; + /// - /// Represents a Concurrent Dictionary for TypeRegistration. + /// Represents a delegate to build an object with the parameters. /// - public class TypesConcurrentDictionary : ConcurrentDictionary - { - private static readonly ConcurrentDictionary ObjectConstructorCache = - new ConcurrentDictionary(); - - private readonly DependencyContainer _dependencyContainer; - - internal TypesConcurrentDictionary(DependencyContainer dependencyContainer) - { - _dependencyContainer = dependencyContainer; - } - - /// - /// Represents a delegate to build an object with the parameters. - /// - /// The parameters. - /// The built object. - public delegate object ObjectConstructor(params object[] parameters); - - internal IEnumerable Resolve(Type resolveType, bool includeUnnamed) - { - var registrations = Keys.Where(tr => tr.Type == resolveType) - .Concat(GetParentRegistrationsForType(resolveType)).Distinct(); - - if (!includeUnnamed) - registrations = registrations.Where(tr => tr.Name != string.Empty); - - return registrations.Select(registration => - ResolveInternal(registration, DependencyContainerResolveOptions.Default)); - } - - internal ObjectFactoryBase GetCurrentFactory(DependencyContainer.TypeRegistration registration) - { - TryGetValue(registration, out var current); - - return current; - } - - internal RegisterOptions Register(Type registerType, string name, ObjectFactoryBase factory) - => AddUpdateRegistration(new DependencyContainer.TypeRegistration(registerType, name), factory); - - internal RegisterOptions AddUpdateRegistration(DependencyContainer.TypeRegistration typeRegistration, ObjectFactoryBase factory) - { - this[typeRegistration] = factory; - - return new RegisterOptions(this, typeRegistration); - } - - internal bool RemoveRegistration(DependencyContainer.TypeRegistration typeRegistration) - => TryRemove(typeRegistration, out _); - - internal object ResolveInternal( - DependencyContainer.TypeRegistration registration, - DependencyContainerResolveOptions options = null) - { - if (options == null) - options = DependencyContainerResolveOptions.Default; - - // Attempt container resolution - if (TryGetValue(registration, out var factory)) - { - try - { - return factory.GetObject(registration.Type, _dependencyContainer, options); - } - catch (DependencyContainerResolutionException) - { - throw; - } - catch (Exception ex) - { - throw new DependencyContainerResolutionException(registration.Type, ex); - } - } - - // Attempt to get a factory from parent if we can - var bubbledObjectFactory = GetParentObjectFactory(registration); - if (bubbledObjectFactory != null) - { - try - { - return bubbledObjectFactory.GetObject(registration.Type, _dependencyContainer, options); - } - catch (DependencyContainerResolutionException) - { - throw; - } - catch (Exception ex) - { - throw new DependencyContainerResolutionException(registration.Type, ex); - } - } - - // Fail if requesting named resolution and settings set to fail if unresolved - if (!string.IsNullOrEmpty(registration.Name) && options.NamedResolutionFailureAction == - DependencyContainerNamedResolutionFailureActions.Fail) - throw new DependencyContainerResolutionException(registration.Type); - - // Attempted unnamed fallback container resolution if relevant and requested - if (!string.IsNullOrEmpty(registration.Name) && options.NamedResolutionFailureAction == - DependencyContainerNamedResolutionFailureActions.AttemptUnnamedResolution) - { - if (TryGetValue(new DependencyContainer.TypeRegistration(registration.Type, string.Empty), out factory)) - { - try - { - return factory.GetObject(registration.Type, _dependencyContainer, options); - } - catch (DependencyContainerResolutionException) - { - throw; - } - catch (Exception ex) - { - throw new DependencyContainerResolutionException(registration.Type, ex); - } - } - } - - // Attempt unregistered construction if possible and requested - var isValid = (options.UnregisteredResolutionAction == - DependencyContainerUnregisteredResolutionActions.AttemptResolve) || - (registration.Type.IsGenericType() && options.UnregisteredResolutionAction == - DependencyContainerUnregisteredResolutionActions.GenericsOnly); - - return isValid && !registration.Type.IsAbstract() && !registration.Type.IsInterface() - ? ConstructType(registration.Type, null, options) - : throw new DependencyContainerResolutionException(registration.Type); - } - - internal bool CanResolve( - DependencyContainer.TypeRegistration registration, - DependencyContainerResolveOptions options = null) - { - if (options == null) - options = DependencyContainerResolveOptions.Default; - - var checkType = registration.Type; - var name = registration.Name; - - if (TryGetValue(new DependencyContainer.TypeRegistration(checkType, name), out var factory)) - { - if (factory.AssumeConstruction) - return true; - - if (factory.Constructor == null) - return GetBestConstructor(factory.CreatesType, options) != null; - - return CanConstruct(factory.Constructor, options); - } - - // Fail if requesting named resolution and settings set to fail if unresolved - // Or bubble up if we have a parent - if (!string.IsNullOrEmpty(name) && options.NamedResolutionFailureAction == - DependencyContainerNamedResolutionFailureActions.Fail) - return _dependencyContainer.Parent?.RegisteredTypes.CanResolve(registration, options.Clone()) ?? false; - - // Attempted unnamed fallback container resolution if relevant and requested - if (!string.IsNullOrEmpty(name) && options.NamedResolutionFailureAction == - DependencyContainerNamedResolutionFailureActions.AttemptUnnamedResolution) - { - if (TryGetValue(new DependencyContainer.TypeRegistration(checkType), out factory)) - { - if (factory.AssumeConstruction) - return true; - - return GetBestConstructor(factory.CreatesType, options) != null; - } - } - - // Check if type is an automatic lazy factory request or an IEnumerable - if (IsAutomaticLazyFactoryRequest(checkType) || registration.Type.IsIEnumerable()) - return true; - - // Attempt unregistered construction if possible and requested - // If we cant', bubble if we have a parent - if ((options.UnregisteredResolutionAction == - DependencyContainerUnregisteredResolutionActions.AttemptResolve) || - (checkType.IsGenericType() && options.UnregisteredResolutionAction == - DependencyContainerUnregisteredResolutionActions.GenericsOnly)) - { - return (GetBestConstructor(checkType, options) != null) || - (_dependencyContainer.Parent?.RegisteredTypes.CanResolve(registration, options.Clone()) ?? false); - } - - // Bubble resolution up the container tree if we have a parent - return _dependencyContainer.Parent != null && _dependencyContainer.Parent.RegisteredTypes.CanResolve(registration, options.Clone()); - } - - internal object ConstructType( - Type implementationType, - ConstructorInfo constructor, - DependencyContainerResolveOptions options = null) - { - var typeToConstruct = implementationType; - - if (constructor == null) - { - // Try and get the best constructor that we can construct - // if we can't construct any then get the constructor - // with the least number of parameters so we can throw a meaningful - // resolve exception - constructor = GetBestConstructor(typeToConstruct, options) ?? - GetTypeConstructors(typeToConstruct).LastOrDefault(); - } - - if (constructor == null) - throw new DependencyContainerResolutionException(typeToConstruct); - - var ctorParams = constructor.GetParameters(); - var args = new object[ctorParams.Length]; - - for (var parameterIndex = 0; parameterIndex < ctorParams.Length; parameterIndex++) - { - var currentParam = ctorParams[parameterIndex]; - - try - { - args[parameterIndex] = options?.ConstructorParameters.GetValueOrDefault(currentParam.Name, ResolveInternal(new DependencyContainer.TypeRegistration(currentParam.ParameterType), options.Clone())); - } - catch (DependencyContainerResolutionException ex) - { - // If a constructor parameter can't be resolved - // it will throw, so wrap it and throw that this can't - // be resolved. - throw new DependencyContainerResolutionException(typeToConstruct, ex); - } - catch (Exception ex) - { - throw new DependencyContainerResolutionException(typeToConstruct, ex); - } - } - - try - { - return CreateObjectConstructionDelegateWithCache(constructor).Invoke(args); - } - catch (Exception ex) - { - throw new DependencyContainerResolutionException(typeToConstruct, ex); - } - } - - private static ObjectConstructor CreateObjectConstructionDelegateWithCache(ConstructorInfo constructor) - { - if (ObjectConstructorCache.TryGetValue(constructor, out var objectConstructor)) - return objectConstructor; - - // We could lock the cache here, but there's no real side - // effect to two threads creating the same ObjectConstructor - // at the same time, compared to the cost of a lock for - // every creation. - var constructorParams = constructor.GetParameters(); - var lambdaParams = Expression.Parameter(typeof(object[]), "parameters"); - var newParams = new Expression[constructorParams.Length]; - - for (var i = 0; i < constructorParams.Length; i++) - { - var paramsParameter = Expression.ArrayIndex(lambdaParams, Expression.Constant(i)); - - newParams[i] = Expression.Convert(paramsParameter, constructorParams[i].ParameterType); - } - - var newExpression = Expression.New(constructor, newParams); - - var constructionLambda = Expression.Lambda(typeof(ObjectConstructor), newExpression, lambdaParams); - - objectConstructor = (ObjectConstructor)constructionLambda.Compile(); - - ObjectConstructorCache[constructor] = objectConstructor; - return objectConstructor; - } - - private static IEnumerable GetTypeConstructors(Type type) - => type.GetConstructors().OrderByDescending(ctor => ctor.GetParameters().Length); - - private static bool IsAutomaticLazyFactoryRequest(Type type) - { - if (!type.IsGenericType()) - return false; - - var genericType = type.GetGenericTypeDefinition(); - - // Just a func - if (genericType == typeof(Func<>)) - return true; - - // 2 parameter func with string as first parameter (name) - if (genericType == typeof(Func<,>) && type.GetGenericArguments()[0] == typeof(string)) - return true; - - // 3 parameter func with string as first parameter (name) and IDictionary as second (parameters) - return genericType == typeof(Func<,,>) && type.GetGenericArguments()[0] == typeof(string) && - type.GetGenericArguments()[1] == typeof(IDictionary); - } - - private ObjectFactoryBase GetParentObjectFactory(DependencyContainer.TypeRegistration registration) - { - if (_dependencyContainer.Parent == null) - return null; - - return _dependencyContainer.Parent.RegisteredTypes.TryGetValue(registration, out var factory) - ? factory.GetFactoryForChildContainer(registration.Type, _dependencyContainer.Parent, _dependencyContainer) - : _dependencyContainer.Parent.RegisteredTypes.GetParentObjectFactory(registration); - } - - private ConstructorInfo GetBestConstructor( - Type type, - DependencyContainerResolveOptions options) - => type.IsValueType() ? null : GetTypeConstructors(type).FirstOrDefault(ctor => CanConstruct(ctor, options)); - - private bool CanConstruct( - ConstructorInfo ctor, - DependencyContainerResolveOptions options) - { - foreach (var parameter in ctor.GetParameters()) - { - if (string.IsNullOrEmpty(parameter.Name)) - return false; - - var isParameterOverload = options.ConstructorParameters.ContainsKey(parameter.Name); - - if (parameter.ParameterType.IsPrimitive() && !isParameterOverload) - return false; - - if (!isParameterOverload && - !CanResolve(new DependencyContainer.TypeRegistration(parameter.ParameterType), options.Clone())) - return false; - } - - return true; - } - - private IEnumerable GetParentRegistrationsForType(Type resolveType) - => _dependencyContainer.Parent == null - ? new DependencyContainer.TypeRegistration[] { } - : _dependencyContainer.Parent.RegisteredTypes.Keys.Where(tr => tr.Type == resolveType).Concat(_dependencyContainer.Parent.RegisteredTypes.GetParentRegistrationsForType(resolveType)); - } + /// The parameters. + /// The built object. + public delegate Object ObjectConstructor(params Object[] parameters); + + internal IEnumerable Resolve(Type resolveType, Boolean includeUnnamed) { + IEnumerable registrations = this.Keys.Where(tr => tr.Type == resolveType) + .Concat(this.GetParentRegistrationsForType(resolveType)).Distinct(); + + if(!includeUnnamed) { + registrations = registrations.Where(tr => tr.Name != String.Empty); + } + + return registrations.Select(registration => + this.ResolveInternal(registration, DependencyContainerResolveOptions.Default)); + } + + internal ObjectFactoryBase GetCurrentFactory(DependencyContainer.TypeRegistration registration) { + _ = this.TryGetValue(registration, out ObjectFactoryBase current); + + return current; + } + + internal RegisterOptions Register(Type registerType, String name, ObjectFactoryBase factory) + => this.AddUpdateRegistration(new DependencyContainer.TypeRegistration(registerType, name), factory); + + internal RegisterOptions AddUpdateRegistration(DependencyContainer.TypeRegistration typeRegistration, ObjectFactoryBase factory) { + this[typeRegistration] = factory; + + return new RegisterOptions(this, typeRegistration); + } + + internal Boolean RemoveRegistration(DependencyContainer.TypeRegistration typeRegistration) + => this.TryRemove(typeRegistration, out _); + + internal Object ResolveInternal( + DependencyContainer.TypeRegistration registration, + DependencyContainerResolveOptions options = null) { + if(options == null) { + options = DependencyContainerResolveOptions.Default; + } + + // Attempt container resolution + if(this.TryGetValue(registration, out ObjectFactoryBase factory)) { + try { + return factory.GetObject(registration.Type, this._dependencyContainer, options); + } catch(DependencyContainerResolutionException) { + throw; + } catch(Exception ex) { + throw new DependencyContainerResolutionException(registration.Type, ex); + } + } + + // Attempt to get a factory from parent if we can + ObjectFactoryBase bubbledObjectFactory = this.GetParentObjectFactory(registration); + if(bubbledObjectFactory != null) { + try { + return bubbledObjectFactory.GetObject(registration.Type, this._dependencyContainer, options); + } catch(DependencyContainerResolutionException) { + throw; + } catch(Exception ex) { + throw new DependencyContainerResolutionException(registration.Type, ex); + } + } + + // Fail if requesting named resolution and settings set to fail if unresolved + if(!String.IsNullOrEmpty(registration.Name) && options.NamedResolutionFailureAction == + DependencyContainerNamedResolutionFailureActions.Fail) { + throw new DependencyContainerResolutionException(registration.Type); + } + + // Attempted unnamed fallback container resolution if relevant and requested + if(!String.IsNullOrEmpty(registration.Name) && options.NamedResolutionFailureAction == + DependencyContainerNamedResolutionFailureActions.AttemptUnnamedResolution) { + if(this.TryGetValue(new DependencyContainer.TypeRegistration(registration.Type, String.Empty), out factory)) { + try { + return factory.GetObject(registration.Type, this._dependencyContainer, options); + } catch(DependencyContainerResolutionException) { + throw; + } catch(Exception ex) { + throw new DependencyContainerResolutionException(registration.Type, ex); + } + } + } + + // Attempt unregistered construction if possible and requested + Boolean isValid = options.UnregisteredResolutionAction == + DependencyContainerUnregisteredResolutionActions.AttemptResolve || + registration.Type.IsGenericType() && options.UnregisteredResolutionAction == + DependencyContainerUnregisteredResolutionActions.GenericsOnly; + + return isValid && !registration.Type.IsAbstract() && !registration.Type.IsInterface() + ? this.ConstructType(registration.Type, null, options) + : throw new DependencyContainerResolutionException(registration.Type); + } + + internal Boolean CanResolve( + DependencyContainer.TypeRegistration registration, + DependencyContainerResolveOptions options = null) { + if(options == null) { + options = DependencyContainerResolveOptions.Default; + } + + Type checkType = registration.Type; + String name = registration.Name; + + if(this.TryGetValue(new DependencyContainer.TypeRegistration(checkType, name), out ObjectFactoryBase factory)) { + return factory.AssumeConstruction + ? true + : factory.Constructor == null + ? this.GetBestConstructor(factory.CreatesType, options) != null + : this.CanConstruct(factory.Constructor, options); + } + + // Fail if requesting named resolution and settings set to fail if unresolved + // Or bubble up if we have a parent + if(!String.IsNullOrEmpty(name) && options.NamedResolutionFailureAction == + DependencyContainerNamedResolutionFailureActions.Fail) { + return this._dependencyContainer.Parent?.RegisteredTypes.CanResolve(registration, options.Clone()) ?? false; + } + + // Attempted unnamed fallback container resolution if relevant and requested + if(!String.IsNullOrEmpty(name) && options.NamedResolutionFailureAction == + DependencyContainerNamedResolutionFailureActions.AttemptUnnamedResolution) { + if(this.TryGetValue(new DependencyContainer.TypeRegistration(checkType), out factory)) { + return factory.AssumeConstruction ? true : this.GetBestConstructor(factory.CreatesType, options) != null; + } + } + + // Check if type is an automatic lazy factory request or an IEnumerable + if(IsAutomaticLazyFactoryRequest(checkType) || registration.Type.IsIEnumerable()) { + return true; + } + + // Attempt unregistered construction if possible and requested + // If we cant', bubble if we have a parent + if(options.UnregisteredResolutionAction == + DependencyContainerUnregisteredResolutionActions.AttemptResolve || + checkType.IsGenericType() && options.UnregisteredResolutionAction == + DependencyContainerUnregisteredResolutionActions.GenericsOnly) { + return this.GetBestConstructor(checkType, options) != null || + (this._dependencyContainer.Parent?.RegisteredTypes.CanResolve(registration, options.Clone()) ?? false); + } + + // Bubble resolution up the container tree if we have a parent + return this._dependencyContainer.Parent != null && this._dependencyContainer.Parent.RegisteredTypes.CanResolve(registration, options.Clone()); + } + + internal Object ConstructType( + Type implementationType, + ConstructorInfo constructor, + DependencyContainerResolveOptions options = null) { + Type typeToConstruct = implementationType; + + if(constructor == null) { + // Try and get the best constructor that we can construct + // if we can't construct any then get the constructor + // with the least number of parameters so we can throw a meaningful + // resolve exception + constructor = this.GetBestConstructor(typeToConstruct, options) ?? + GetTypeConstructors(typeToConstruct).LastOrDefault(); + } + + if(constructor == null) { + throw new DependencyContainerResolutionException(typeToConstruct); + } + + ParameterInfo[] ctorParams = constructor.GetParameters(); + Object[] args = new Object[ctorParams.Length]; + + for(Int32 parameterIndex = 0; parameterIndex < ctorParams.Length; parameterIndex++) { + ParameterInfo currentParam = ctorParams[parameterIndex]; + + try { + args[parameterIndex] = options?.ConstructorParameters.GetValueOrDefault(currentParam.Name, this.ResolveInternal(new DependencyContainer.TypeRegistration(currentParam.ParameterType), options.Clone())); + } catch(DependencyContainerResolutionException ex) { + // If a constructor parameter can't be resolved + // it will throw, so wrap it and throw that this can't + // be resolved. + throw new DependencyContainerResolutionException(typeToConstruct, ex); + } catch(Exception ex) { + throw new DependencyContainerResolutionException(typeToConstruct, ex); + } + } + + try { + return CreateObjectConstructionDelegateWithCache(constructor).Invoke(args); + } catch(Exception ex) { + throw new DependencyContainerResolutionException(typeToConstruct, ex); + } + } + + private static ObjectConstructor CreateObjectConstructionDelegateWithCache(ConstructorInfo constructor) { + if(ObjectConstructorCache.TryGetValue(constructor, out ObjectConstructor objectConstructor)) { + return objectConstructor; + } + + // We could lock the cache here, but there's no real side + // effect to two threads creating the same ObjectConstructor + // at the same time, compared to the cost of a lock for + // every creation. + ParameterInfo[] constructorParams = constructor.GetParameters(); + ParameterExpression lambdaParams = Expression.Parameter(typeof(Object[]), "parameters"); + Expression[] newParams = new Expression[constructorParams.Length]; + + for(Int32 i = 0; i < constructorParams.Length; i++) { + BinaryExpression paramsParameter = Expression.ArrayIndex(lambdaParams, Expression.Constant(i)); + + newParams[i] = Expression.Convert(paramsParameter, constructorParams[i].ParameterType); + } + + NewExpression newExpression = Expression.New(constructor, newParams); + + LambdaExpression constructionLambda = Expression.Lambda(typeof(ObjectConstructor), newExpression, lambdaParams); + + objectConstructor = (ObjectConstructor)constructionLambda.Compile(); + + ObjectConstructorCache[constructor] = objectConstructor; + return objectConstructor; + } + + private static IEnumerable GetTypeConstructors(Type type) + => type.GetConstructors().OrderByDescending(ctor => ctor.GetParameters().Length); + + private static Boolean IsAutomaticLazyFactoryRequest(Type type) { + if(!type.IsGenericType()) { + return false; + } + + Type genericType = type.GetGenericTypeDefinition(); + + // Just a func + if(genericType == typeof(Func<>)) { + return true; + } + + // 2 parameter func with string as first parameter (name) + if(genericType == typeof(Func<,>) && type.GetGenericArguments()[0] == typeof(String)) { + return true; + } + + // 3 parameter func with string as first parameter (name) and IDictionary as second (parameters) + return genericType == typeof(Func<,,>) && type.GetGenericArguments()[0] == typeof(String) && + type.GetGenericArguments()[1] == typeof(IDictionary); + } + + private ObjectFactoryBase GetParentObjectFactory(DependencyContainer.TypeRegistration registration) => this._dependencyContainer.Parent == null + ? null + : this._dependencyContainer.Parent.RegisteredTypes.TryGetValue(registration, out ObjectFactoryBase factory) + ? factory.GetFactoryForChildContainer(registration.Type, this._dependencyContainer.Parent, this._dependencyContainer) + : this._dependencyContainer.Parent.RegisteredTypes.GetParentObjectFactory(registration); + + private ConstructorInfo GetBestConstructor( + Type type, + DependencyContainerResolveOptions options) + => type.IsValueType() ? null : GetTypeConstructors(type).FirstOrDefault(ctor => this.CanConstruct(ctor, options)); + + private Boolean CanConstruct( + ConstructorInfo ctor, + DependencyContainerResolveOptions options) { + foreach(ParameterInfo parameter in ctor.GetParameters()) { + if(String.IsNullOrEmpty(parameter.Name)) { + return false; + } + + Boolean isParameterOverload = options.ConstructorParameters.ContainsKey(parameter.Name); + + if(parameter.ParameterType.IsPrimitive() && !isParameterOverload) { + return false; + } + + if(!isParameterOverload && + !this.CanResolve(new DependencyContainer.TypeRegistration(parameter.ParameterType), options.Clone())) { + return false; + } + } + + return true; + } + + private IEnumerable GetParentRegistrationsForType(Type resolveType) + => this._dependencyContainer.Parent == null + ? new DependencyContainer.TypeRegistration[] { } + : this._dependencyContainer.Parent.RegisteredTypes.Keys.Where(tr => tr.Type == resolveType).Concat(this._dependencyContainer.Parent.RegisteredTypes.GetParentRegistrationsForType(resolveType)); + } } diff --git a/Unosquare.Swan/Enums.cs b/Unosquare.Swan/Enums.cs index 6cb034f..1aadef6 100644 --- a/Unosquare.Swan/Enums.cs +++ b/Unosquare.Swan/Enums.cs @@ -1,28 +1,26 @@ -namespace Unosquare.Swan -{ +namespace Unosquare.Swan { + /// + /// Enumerates the possible causes of the DataReceived event occurring. + /// + public enum ConnectionDataReceivedTrigger { /// - /// Enumerates the possible causes of the DataReceived event occurring. + /// The trigger was a forceful flush of the buffer /// - public enum ConnectionDataReceivedTrigger - { - /// - /// The trigger was a forceful flush of the buffer - /// - Flush, - - /// - /// The new line sequence bytes were received - /// - NewLineSequenceEncountered, - - /// - /// The buffer was full - /// - BufferFull, - - /// - /// The block size reached - /// - BlockSizeReached, - } + Flush, + + /// + /// The new line sequence bytes were received + /// + NewLineSequenceEncountered, + + /// + /// The buffer was full + /// + BufferFull, + + /// + /// The block size reached + /// + BlockSizeReached, + } } diff --git a/Unosquare.Swan/Eventing.ConnectionListener.cs b/Unosquare.Swan/Eventing.ConnectionListener.cs index d7e8b96..3b3ac81 100644 --- a/Unosquare.Swan/Eventing.ConnectionListener.cs +++ b/Unosquare.Swan/Eventing.ConnectionListener.cs @@ -1,158 +1,157 @@ -namespace Unosquare.Swan -{ - using System; - using System.Net; - using System.Net.Sockets; - +using System; +using System.Net; +using System.Net.Sockets; + +namespace Unosquare.Swan { + /// + /// The event arguments for when connections are accepted. + /// + /// + public class ConnectionAcceptedEventArgs : EventArgs { /// - /// The event arguments for when connections are accepted. + /// Initializes a new instance of the class. /// - /// - public class ConnectionAcceptedEventArgs : EventArgs - { - /// - /// Initializes a new instance of the class. - /// - /// The client. - /// client. - public ConnectionAcceptedEventArgs(TcpClient client) - { - Client = client ?? throw new ArgumentNullException(nameof(client)); - } - - /// - /// Gets the client. - /// - /// - /// The client. - /// - public TcpClient Client { get; } - } - + /// The client. + /// client. + public ConnectionAcceptedEventArgs(TcpClient client) => this.Client = client ?? throw new ArgumentNullException(nameof(client)); + /// - /// Occurs before a connection is accepted. Set the Cancel property to true to prevent the connection from being accepted. + /// Gets the client. /// - /// - public class ConnectionAcceptingEventArgs : ConnectionAcceptedEventArgs - { - /// - /// Initializes a new instance of the class. - /// - /// The client. - public ConnectionAcceptingEventArgs(TcpClient client) - : base(client) - { - } - - /// - /// Setting Cancel to true rejects the new TcpClient. - /// - /// - /// true if cancel; otherwise, false. - /// - public bool Cancel { get; set; } - } - + /// + /// The client. + /// + public TcpClient Client { + get; + } + } + + /// + /// Occurs before a connection is accepted. Set the Cancel property to true to prevent the connection from being accepted. + /// + /// + public class ConnectionAcceptingEventArgs : ConnectionAcceptedEventArgs { /// - /// Event arguments for when a server listener is started. + /// Initializes a new instance of the class. /// - /// - public class ConnectionListenerStartedEventArgs : EventArgs - { - /// - /// Initializes a new instance of the class. - /// - /// The listener end point. - /// listenerEndPoint. - public ConnectionListenerStartedEventArgs(IPEndPoint listenerEndPoint) - { - EndPoint = listenerEndPoint ?? throw new ArgumentNullException(nameof(listenerEndPoint)); - } - - /// - /// Gets the end point. - /// - /// - /// The end point. - /// - public IPEndPoint EndPoint { get; } - } - + /// The client. + public ConnectionAcceptingEventArgs(TcpClient client) + : base(client) { + } + /// - /// Event arguments for when a server listener fails to start. + /// Setting Cancel to true rejects the new TcpClient. /// - /// - public class ConnectionListenerFailedEventArgs : EventArgs - { - /// - /// Initializes a new instance of the class. - /// - /// The listener end point. - /// The ex. - /// - /// listenerEndPoint - /// or - /// ex. - /// - public ConnectionListenerFailedEventArgs(IPEndPoint listenerEndPoint, Exception ex) - { - EndPoint = listenerEndPoint ?? throw new ArgumentNullException(nameof(listenerEndPoint)); - Error = ex ?? throw new ArgumentNullException(nameof(ex)); - } - - /// - /// Gets the end point. - /// - /// - /// The end point. - /// - public IPEndPoint EndPoint { get; } - - /// - /// Gets the error. - /// - /// - /// The error. - /// - public Exception Error { get; } - } - + /// + /// true if cancel; otherwise, false. + /// + public Boolean Cancel { + get; set; + } + } + + /// + /// Event arguments for when a server listener is started. + /// + /// + public class ConnectionListenerStartedEventArgs : EventArgs { /// - /// Event arguments for when a server listener stopped. + /// Initializes a new instance of the class. /// - /// - public class ConnectionListenerStoppedEventArgs : EventArgs - { - /// - /// Initializes a new instance of the class. - /// - /// The listener end point. - /// The ex. - /// - /// listenerEndPoint - /// or - /// ex. - /// - public ConnectionListenerStoppedEventArgs(IPEndPoint listenerEndPoint, Exception ex = null) - { - EndPoint = listenerEndPoint ?? throw new ArgumentNullException(nameof(listenerEndPoint)); - Error = ex; - } - - /// - /// Gets the end point. - /// - /// - /// The end point. - /// - public IPEndPoint EndPoint { get; } - - /// - /// Gets the error. - /// - /// - /// The error. - /// - public Exception Error { get; } - } + /// The listener end point. + /// listenerEndPoint. + public ConnectionListenerStartedEventArgs(IPEndPoint listenerEndPoint) => this.EndPoint = listenerEndPoint ?? throw new ArgumentNullException(nameof(listenerEndPoint)); + + /// + /// Gets the end point. + /// + /// + /// The end point. + /// + public IPEndPoint EndPoint { + get; + } + } + + /// + /// Event arguments for when a server listener fails to start. + /// + /// + public class ConnectionListenerFailedEventArgs : EventArgs { + /// + /// Initializes a new instance of the class. + /// + /// The listener end point. + /// The ex. + /// + /// listenerEndPoint + /// or + /// ex. + /// + public ConnectionListenerFailedEventArgs(IPEndPoint listenerEndPoint, Exception ex) { + this.EndPoint = listenerEndPoint ?? throw new ArgumentNullException(nameof(listenerEndPoint)); + this.Error = ex ?? throw new ArgumentNullException(nameof(ex)); + } + + /// + /// Gets the end point. + /// + /// + /// The end point. + /// + public IPEndPoint EndPoint { + get; + } + + /// + /// Gets the error. + /// + /// + /// The error. + /// + public Exception Error { + get; + } + } + + /// + /// Event arguments for when a server listener stopped. + /// + /// + public class ConnectionListenerStoppedEventArgs : EventArgs { + /// + /// Initializes a new instance of the class. + /// + /// The listener end point. + /// The ex. + /// + /// listenerEndPoint + /// or + /// ex. + /// + public ConnectionListenerStoppedEventArgs(IPEndPoint listenerEndPoint, Exception ex = null) { + this.EndPoint = listenerEndPoint ?? throw new ArgumentNullException(nameof(listenerEndPoint)); + this.Error = ex; + } + + /// + /// Gets the end point. + /// + /// + /// The end point. + /// + public IPEndPoint EndPoint { + get; + } + + /// + /// Gets the error. + /// + /// + /// The error. + /// + public Exception Error { + get; + } + } } \ No newline at end of file diff --git a/Unosquare.Swan/Eventing.cs b/Unosquare.Swan/Eventing.cs index a460255..b4d590b 100644 --- a/Unosquare.Swan/Eventing.cs +++ b/Unosquare.Swan/Eventing.cs @@ -1,90 +1,91 @@ -namespace Unosquare.Swan -{ - using System; - using System.Text; - +using System; +using System.Text; + +namespace Unosquare.Swan { + /// + /// The event arguments for connection failure events. + /// + /// + public class ConnectionFailureEventArgs : EventArgs { /// - /// The event arguments for connection failure events. + /// Initializes a new instance of the class. /// - /// - public class ConnectionFailureEventArgs : EventArgs - { - /// - /// Initializes a new instance of the class. - /// - /// The ex. - public ConnectionFailureEventArgs(Exception ex) - { - Error = ex; - } - - /// - /// Gets the error. - /// - /// - /// The error. - /// - public Exception Error { get; } - } - + /// The ex. + public ConnectionFailureEventArgs(Exception ex) => this.Error = ex; + /// - /// Event arguments for when data is received. + /// Gets the error. /// - /// - public class ConnectionDataReceivedEventArgs : EventArgs - { - /// - /// Initializes a new instance of the class. - /// - /// The buffer. - /// The trigger. - /// if set to true [more available]. - public ConnectionDataReceivedEventArgs(byte[] buffer, ConnectionDataReceivedTrigger trigger, bool moreAvailable) - { - Buffer = buffer ?? throw new ArgumentNullException(nameof(buffer)); - Trigger = trigger; - HasMoreAvailable = moreAvailable; - } - - /// - /// Gets the buffer. - /// - /// - /// The buffer. - /// - public byte[] Buffer { get; } - - /// - /// Gets the cause as to why this event was thrown. - /// - /// - /// The trigger. - /// - public ConnectionDataReceivedTrigger Trigger { get; } - - /// - /// Gets a value indicating whether the receive buffer has more bytes available. - /// - /// - /// true if this instance has more available; otherwise, false. - /// - public bool HasMoreAvailable { get; } - - /// - /// Gets the string from the given buffer. - /// - /// The buffer. - /// The encoding. - /// A that contains the results of decoding the specified sequence of bytes. - public static string GetStringFromBuffer(byte[] buffer, Encoding encoding) - => encoding.GetString(buffer).TrimEnd('\r', '\n'); - - /// - /// Gets the string from buffer. - /// - /// The encoding. - /// A that contains the results of decoding the specified sequence of bytes. - public string GetStringFromBuffer(Encoding encoding) - => GetStringFromBuffer(Buffer, encoding); - } + /// + /// The error. + /// + public Exception Error { + get; + } + } + + /// + /// Event arguments for when data is received. + /// + /// + public class ConnectionDataReceivedEventArgs : EventArgs { + /// + /// Initializes a new instance of the class. + /// + /// The buffer. + /// The trigger. + /// if set to true [more available]. + public ConnectionDataReceivedEventArgs(Byte[] buffer, ConnectionDataReceivedTrigger trigger, Boolean moreAvailable) { + this.Buffer = buffer ?? throw new ArgumentNullException(nameof(buffer)); + this.Trigger = trigger; + this.HasMoreAvailable = moreAvailable; + } + + /// + /// Gets the buffer. + /// + /// + /// The buffer. + /// + public Byte[] Buffer { + get; + } + + /// + /// Gets the cause as to why this event was thrown. + /// + /// + /// The trigger. + /// + public ConnectionDataReceivedTrigger Trigger { + get; + } + + /// + /// Gets a value indicating whether the receive buffer has more bytes available. + /// + /// + /// true if this instance has more available; otherwise, false. + /// + public Boolean HasMoreAvailable { + get; + } + + /// + /// Gets the string from the given buffer. + /// + /// The buffer. + /// The encoding. + /// A that contains the results of decoding the specified sequence of bytes. + public static String GetStringFromBuffer(Byte[] buffer, Encoding encoding) + => encoding.GetString(buffer).TrimEnd('\r', '\n'); + + /// + /// Gets the string from buffer. + /// + /// The encoding. + /// A that contains the results of decoding the specified sequence of bytes. + public String GetStringFromBuffer(Encoding encoding) + => GetStringFromBuffer(this.Buffer, encoding); + } } diff --git a/Unosquare.Swan/Exceptions/DependencyContainerRegistrationException.cs b/Unosquare.Swan/Exceptions/DependencyContainerRegistrationException.cs index 5d13d2c..ccae27f 100644 --- a/Unosquare.Swan/Exceptions/DependencyContainerRegistrationException.cs +++ b/Unosquare.Swan/Exceptions/DependencyContainerRegistrationException.cs @@ -1,46 +1,39 @@ -namespace Unosquare.Swan.Exceptions -{ - using System; - using System.Collections.Generic; - using System.Linq; - +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Unosquare.Swan.Exceptions { + /// + /// Generic Constraint Registration Exception. + /// + /// + public class DependencyContainerRegistrationException : Exception { + private const String ConvertErrorText = "Cannot convert current registration of {0} to {1}"; + private const String RegisterErrorText = + "Cannot register type {0} - abstract classes or interfaces are not valid implementation types for {1}."; + private const String ErrorText = "Duplicate implementation of type {0} found ({1})."; + /// - /// Generic Constraint Registration Exception. + /// Initializes a new instance of the class. /// - /// - public class DependencyContainerRegistrationException : Exception - { - private const string ConvertErrorText = "Cannot convert current registration of {0} to {1}"; - private const string RegisterErrorText = - "Cannot register type {0} - abstract classes or interfaces are not valid implementation types for {1}."; - private const string ErrorText = "Duplicate implementation of type {0} found ({1})."; - - /// - /// Initializes a new instance of the class. - /// - /// Type of the register. - /// The types. - public DependencyContainerRegistrationException(Type registerType, IEnumerable types) - : base(string.Format(ErrorText, registerType, GetTypesString(types))) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The type. - /// The method. - /// if set to true [is type factory]. - public DependencyContainerRegistrationException(Type type, string method, bool isTypeFactory = false) - : base(isTypeFactory - ? string.Format(RegisterErrorText, type.FullName, method) - : string.Format(ConvertErrorText, type.FullName, method)) - { - } - - private static string GetTypesString(IEnumerable types) - { - return string.Join(",", types.Select(type => type.FullName)); - } - } + /// Type of the register. + /// The types. + public DependencyContainerRegistrationException(Type registerType, IEnumerable types) + : base(String.Format(ErrorText, registerType, GetTypesString(types))) { + } + + /// + /// Initializes a new instance of the class. + /// + /// The type. + /// The method. + /// if set to true [is type factory]. + public DependencyContainerRegistrationException(Type type, String method, Boolean isTypeFactory = false) + : base(isTypeFactory + ? String.Format(RegisterErrorText, type.FullName, method) + : String.Format(ConvertErrorText, type.FullName, method)) { + } + + private static String GetTypesString(IEnumerable types) => String.Join(",", types.Select(type => type.FullName)); + } } \ No newline at end of file diff --git a/Unosquare.Swan/Exceptions/DependencyContainerResolutionException.cs b/Unosquare.Swan/Exceptions/DependencyContainerResolutionException.cs index b74bd8e..2fd6cf1 100644 --- a/Unosquare.Swan/Exceptions/DependencyContainerResolutionException.cs +++ b/Unosquare.Swan/Exceptions/DependencyContainerResolutionException.cs @@ -1,32 +1,28 @@ -namespace Unosquare.Swan.Exceptions -{ - using System; - +using System; + +namespace Unosquare.Swan.Exceptions { + /// + /// An exception for dependency resolutions. + /// + /// + public class DependencyContainerResolutionException : Exception { + private const String ErrorText = "Unable to resolve type: {0}"; + /// - /// An exception for dependency resolutions. + /// Initializes a new instance of the class. /// - /// - public class DependencyContainerResolutionException : Exception - { - private const string ErrorText = "Unable to resolve type: {0}"; - - /// - /// Initializes a new instance of the class. - /// - /// The type. - public DependencyContainerResolutionException(Type type) - : base(string.Format(ErrorText, type.FullName)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The type. - /// The inner exception. - public DependencyContainerResolutionException(Type type, Exception innerException) - : base(string.Format(ErrorText, type.FullName), innerException) - { - } - } + /// The type. + public DependencyContainerResolutionException(Type type) + : base(String.Format(ErrorText, type.FullName)) { + } + + /// + /// Initializes a new instance of the class. + /// + /// The type. + /// The inner exception. + public DependencyContainerResolutionException(Type type, Exception innerException) + : base(String.Format(ErrorText, type.FullName), innerException) { + } + } } diff --git a/Unosquare.Swan/Exceptions/DependencyContainerWeakReferenceException.cs b/Unosquare.Swan/Exceptions/DependencyContainerWeakReferenceException.cs index 52a2974..1876056 100644 --- a/Unosquare.Swan/Exceptions/DependencyContainerWeakReferenceException.cs +++ b/Unosquare.Swan/Exceptions/DependencyContainerWeakReferenceException.cs @@ -1,22 +1,19 @@ -namespace Unosquare.Swan.Exceptions -{ - using System; - +using System; + +namespace Unosquare.Swan.Exceptions { + /// + /// Weak Reference Exception. + /// + /// + public class DependencyContainerWeakReferenceException : Exception { + private const String ErrorText = "Unable to instantiate {0} - referenced object has been reclaimed"; + /// - /// Weak Reference Exception. + /// Initializes a new instance of the class. /// - /// - public class DependencyContainerWeakReferenceException : Exception - { - private const string ErrorText = "Unable to instantiate {0} - referenced object has been reclaimed"; - - /// - /// Initializes a new instance of the class. - /// - /// The type. - public DependencyContainerWeakReferenceException(Type type) - : base(string.Format(ErrorText, type.FullName)) - { - } - } + /// The type. + public DependencyContainerWeakReferenceException(Type type) + : base(String.Format(ErrorText, type.FullName)) { + } + } } diff --git a/Unosquare.Swan/Exceptions/DnsQueryException.cs b/Unosquare.Swan/Exceptions/DnsQueryException.cs index 5ee5264..a23f8a9 100644 --- a/Unosquare.Swan/Exceptions/DnsQueryException.cs +++ b/Unosquare.Swan/Exceptions/DnsQueryException.cs @@ -1,40 +1,31 @@ -namespace Unosquare.Swan.Exceptions -{ - using System; - using Networking; - - /// - /// An exception thrown when the DNS query fails. - /// - /// - public class DnsQueryException : Exception - { - internal DnsQueryException(string message) - : base(message) - { - } - - internal DnsQueryException(string message, Exception e) - : base(message, e) - { - } - - internal DnsQueryException(DnsClient.IDnsResponse response) - : this(response, Format(response)) - { - } - - internal DnsQueryException(DnsClient.IDnsResponse response, string message) - : base(message) - { - Response = response; - } - - internal DnsClient.IDnsResponse Response { get; } - - private static string Format(DnsClient.IDnsResponse response) - { - return $"Invalid response received with code {response.ResponseCode}"; - } - } +using System; +using Unosquare.Swan.Networking; + +namespace Unosquare.Swan.Exceptions { + /// + /// An exception thrown when the DNS query fails. + /// + /// + public class DnsQueryException : Exception { + internal DnsQueryException(String message) + : base(message) { + } + + internal DnsQueryException(String message, Exception e) + : base(message, e) { + } + + internal DnsQueryException(DnsClient.IDnsResponse response) + : this(response, Format(response)) { + } + + internal DnsQueryException(DnsClient.IDnsResponse response, String message) + : base(message) => this.Response = response; + + internal DnsClient.IDnsResponse Response { + get; + } + + private static String Format(DnsClient.IDnsResponse response) => $"Invalid response received with code {response.ResponseCode}"; + } } \ No newline at end of file diff --git a/Unosquare.Swan/Exceptions/JsonRequestException.cs b/Unosquare.Swan/Exceptions/JsonRequestException.cs index 0ae0949..726cfdf 100644 --- a/Unosquare.Swan/Exceptions/JsonRequestException.cs +++ b/Unosquare.Swan/Exceptions/JsonRequestException.cs @@ -1,48 +1,49 @@ -namespace Unosquare.Swan.Exceptions -{ - using System; - - /// - /// Represents errors that occurs requesting a JSON file through HTTP. - /// - /// -#if !NETSTANDARD1_3 - [Serializable] +using System; + +namespace Unosquare.Swan.Exceptions { + /// + /// Represents errors that occurs requesting a JSON file through HTTP. + /// + /// +#if !NETSTANDARD1_3 + [Serializable] #endif - public class JsonRequestException - : Exception - { - /// - /// - /// Initializes a new instance of the class. - /// - /// The message. - /// The HTTP error code. - /// Content of the error. - public JsonRequestException(string message, int httpErrorCode = 500, string errorContent = null) - : base(message) - { - HttpErrorCode = httpErrorCode; - HttpErrorContent = errorContent; - } - - /// - /// Gets the HTTP error code. - /// - /// - /// The HTTP error code. - /// - public int HttpErrorCode { get; } - - /// - /// Gets the content of the HTTP error. - /// - /// - /// The content of the HTTP error. - /// - public string HttpErrorContent { get; } - - /// - public override string ToString() => string.IsNullOrEmpty(HttpErrorContent) ? $"HTTP Response Status Code {HttpErrorCode} Error Message: {HttpErrorContent}" : base.ToString(); - } + public class JsonRequestException + : Exception { + /// + /// + /// Initializes a new instance of the class. + /// + /// The message. + /// The HTTP error code. + /// Content of the error. + public JsonRequestException(String message, Int32 httpErrorCode = 500, String errorContent = null) + : base(message) { + this.HttpErrorCode = httpErrorCode; + this.HttpErrorContent = errorContent; + } + + /// + /// Gets the HTTP error code. + /// + /// + /// The HTTP error code. + /// + public Int32 HttpErrorCode { + get; + } + + /// + /// Gets the content of the HTTP error. + /// + /// + /// The content of the HTTP error. + /// + public String HttpErrorContent { + get; + } + + /// + public override String ToString() => String.IsNullOrEmpty(this.HttpErrorContent) ? $"HTTP Response Status Code {this.HttpErrorCode} Error Message: {this.HttpErrorContent}" : base.ToString(); + } } diff --git a/Unosquare.Swan/Exceptions/LdapException.cs b/Unosquare.Swan/Exceptions/LdapException.cs index 7575818..e43a5b5 100644 --- a/Unosquare.Swan/Exceptions/LdapException.cs +++ b/Unosquare.Swan/Exceptions/LdapException.cs @@ -1,134 +1,133 @@ -namespace Unosquare.Swan.Exceptions -{ - using System; - using Networking.Ldap; - +using System; +using Unosquare.Swan.Networking.Ldap; + +namespace Unosquare.Swan.Exceptions { + /// + /// Thrown to indicate that an Ldap exception has occurred. This is a general + /// exception which includes a message and an Ldap result code. + /// An LdapException can result from physical problems (such as + /// network errors) as well as problems with Ldap operations detected + /// by the server. For example, if an Ldap add operation fails because of a + /// duplicate entry, the server returns a result code. + /// + /// + public class LdapException + : Exception { + internal const String UnexpectedEnd = "Unexpected end of filter"; + internal const String MissingLeftParen = "Unmatched parentheses, left parenthesis missing"; + internal const String MissingRightParen = "Unmatched parentheses, right parenthesis missing"; + internal const String ExpectingRightParen = "Expecting right parenthesis, found \"{0}\""; + internal const String ExpectingLeftParen = "Expecting left parenthesis, found \"{0}\""; + + private readonly String _serverMessage; + /// - /// Thrown to indicate that an Ldap exception has occurred. This is a general - /// exception which includes a message and an Ldap result code. - /// An LdapException can result from physical problems (such as - /// network errors) as well as problems with Ldap operations detected - /// by the server. For example, if an Ldap add operation fails because of a - /// duplicate entry, the server returns a result code. + /// Initializes a new instance of the class. + /// Constructs an exception with a detailed message obtained from the + /// specified MessageOrKey String. + /// Additional parameters specify the result code, the message returned + /// from the server, and a matchedDN returned from the server. + /// The String is used either as a message key to obtain a localized + /// message from ExceptionMessages, or if there is no key in the + /// resource matching the text, it is used as the detailed message itself. /// - /// - public class LdapException - : Exception - { - internal const string UnexpectedEnd = "Unexpected end of filter"; - internal const string MissingLeftParen = "Unmatched parentheses, left parenthesis missing"; - internal const string MissingRightParen = "Unmatched parentheses, right parenthesis missing"; - internal const string ExpectingRightParen = "Expecting right parenthesis, found \"{0}\""; - internal const string ExpectingLeftParen = "Expecting left parenthesis, found \"{0}\""; - - private readonly string _serverMessage; - - /// - /// Initializes a new instance of the class. - /// Constructs an exception with a detailed message obtained from the - /// specified MessageOrKey String. - /// Additional parameters specify the result code, the message returned - /// from the server, and a matchedDN returned from the server. - /// The String is used either as a message key to obtain a localized - /// message from ExceptionMessages, or if there is no key in the - /// resource matching the text, it is used as the detailed message itself. - /// - /// The message. - /// The result code returned. - /// Error message specifying additional information - /// from the server. - /// The maximal subset of a specified DN which could - /// be matched by the server on a search operation. - /// The root exception. - public LdapException( - string message, - LdapStatusCode resultCode, - string serverMsg = null, - string matchedDN = null, - Exception rootException = null) - : base(message) - { - ResultCode = resultCode; - Cause = rootException; - MatchedDN = matchedDN; - _serverMessage = serverMsg; - } - - /// - /// Returns the error message from the Ldap server, if this message is - /// available (that is, if this message was set). If the message was not set, - /// this method returns null. - /// - /// - /// The error message or null if the message was not set. - /// - public string LdapErrorMessage => - _serverMessage != null && _serverMessage.Length == 0 ? null : _serverMessage; - - /// - /// Returns the lower level Exception which caused the failure, if any. - /// For example, an IOException with additional information may be returned - /// on a CONNECT_ERROR failure. - /// - /// - /// The cause. - /// - public Exception Cause { get; } - - /// - /// Returns the result code from the exception. - /// The codes are defined as public final static int members - /// of the Ldap Exception class. If the exception is a - /// result of error information returned from a directory operation, the - /// code will be one of those defined for the class. Otherwise, a local error - /// code is returned. - /// - /// - /// The result code. - /// - public LdapStatusCode ResultCode { get; } - - /// - /// Returns the part of a submitted distinguished name which could be - /// matched by the server. - /// If the exception was caused by a local error, such as no server - /// available, the return value is null. If the exception resulted from - /// an operation being executed on a server, the value is an empty string - /// except when the result of the operation was one of the following:. - ///
  • NO_SUCH_OBJECT
  • ALIAS_PROBLEM
  • INVALID_DN_SYNTAX
  • ALIAS_DEREFERENCING_PROBLEM
- ///
- /// - /// The matched dn. - /// - public string MatchedDN { get; } - - /// - public override string Message => ResultCode.ToString().Humanize(); - - /// - public override string ToString() - { - // Craft a string from the resource file - var msg = $"{nameof(LdapException)}: {base.Message} ({ResultCode}) {ResultCode.ToString().Humanize()}"; - - // Add server message - if (!string.IsNullOrEmpty(_serverMessage)) - { - msg += $"\r\nServer Message: {_serverMessage}"; - } - - // Add Matched DN message - if (MatchedDN != null) - { - msg += $"\r\nMatched DN: {MatchedDN}"; - } - - if (Cause != null) - { - msg += $"\r\n{Cause}"; - } - - return msg; - } - } + /// The message. + /// The result code returned. + /// Error message specifying additional information + /// from the server. + /// The maximal subset of a specified DN which could + /// be matched by the server on a search operation. + /// The root exception. + public LdapException( + String message, + LdapStatusCode resultCode, + String serverMsg = null, + String matchedDN = null, + Exception rootException = null) + : base(message) { + this.ResultCode = resultCode; + this.Cause = rootException; + this.MatchedDN = matchedDN; + this._serverMessage = serverMsg; + } + + /// + /// Returns the error message from the Ldap server, if this message is + /// available (that is, if this message was set). If the message was not set, + /// this method returns null. + /// + /// + /// The error message or null if the message was not set. + /// + public String LdapErrorMessage => + this._serverMessage != null && this._serverMessage.Length == 0 ? null : this._serverMessage; + + /// + /// Returns the lower level Exception which caused the failure, if any. + /// For example, an IOException with additional information may be returned + /// on a CONNECT_ERROR failure. + /// + /// + /// The cause. + /// + public Exception Cause { + get; + } + + /// + /// Returns the result code from the exception. + /// The codes are defined as public final static int members + /// of the Ldap Exception class. If the exception is a + /// result of error information returned from a directory operation, the + /// code will be one of those defined for the class. Otherwise, a local error + /// code is returned. + /// + /// + /// The result code. + /// + public LdapStatusCode ResultCode { + get; + } + + /// + /// Returns the part of a submitted distinguished name which could be + /// matched by the server. + /// If the exception was caused by a local error, such as no server + /// available, the return value is null. If the exception resulted from + /// an operation being executed on a server, the value is an empty string + /// except when the result of the operation was one of the following:. + ///
  • NO_SUCH_OBJECT
  • ALIAS_PROBLEM
  • INVALID_DN_SYNTAX
  • ALIAS_DEREFERENCING_PROBLEM
+ ///
+ /// + /// The matched dn. + /// + public String MatchedDN { + get; + } + + /// + public override String Message => this.ResultCode.ToString().Humanize(); + + /// + public override String ToString() { + // Craft a string from the resource file + String msg = $"{nameof(LdapException)}: {base.Message} ({this.ResultCode}) {this.ResultCode.ToString().Humanize()}"; + + // Add server message + if(!String.IsNullOrEmpty(this._serverMessage)) { + msg += $"\r\nServer Message: {this._serverMessage}"; + } + + // Add Matched DN message + if(this.MatchedDN != null) { + msg += $"\r\nMatched DN: {this.MatchedDN}"; + } + + if(this.Cause != null) { + msg += $"\r\n{this.Cause}"; + } + + return msg; + } + } } \ No newline at end of file diff --git a/Unosquare.Swan/Extensions.MimeMessage.cs b/Unosquare.Swan/Extensions.MimeMessage.cs index e480531..e69e24e 100644 --- a/Unosquare.Swan/Extensions.MimeMessage.cs +++ b/Unosquare.Swan/Extensions.MimeMessage.cs @@ -1,55 +1,51 @@ #if NET452 || NETSTANDARD2_0 -namespace Unosquare.Swan -{ - using System; - using System.IO; - using System.Net.Mail; - using System.Reflection; - +using System; +using System.IO; +using System.Net.Mail; +using System.Reflection; +namespace Unosquare.Swan { + /// + /// Extension methods. + /// + public static class SmtpExtensions { /// - /// Extension methods. + /// The raw contents of this MailMessage as a MemoryStream. /// - public static class SmtpExtensions - { - /// - /// The raw contents of this MailMessage as a MemoryStream. - /// - /// The caller. - /// A MemoryStream with the raw contents of this MailMessage. - public static MemoryStream ToMimeMessage(this MailMessage self) - { - if (self == null) - throw new ArgumentNullException(nameof(self)); - - var result = new MemoryStream(); - var mailWriter = MimeMessageConstants.MailWriterConstructor.Invoke(new object[] { result }); - MimeMessageConstants.SendMethod.Invoke( - self, - MimeMessageConstants.PrivateInstanceFlags, - null, - MimeMessageConstants.IsRunningInDotNetFourPointFive ? new[] { mailWriter, true, true } : new[] { mailWriter, true }, - null); - - result = new MemoryStream(result.ToArray()); - MimeMessageConstants.CloseMethod.Invoke( - mailWriter, - MimeMessageConstants.PrivateInstanceFlags, - null, - new object[] { }, - null); - result.Position = 0; - return result; - } - - internal static class MimeMessageConstants - { - public static readonly BindingFlags PrivateInstanceFlags = BindingFlags.Instance | BindingFlags.NonPublic; - public static readonly Type MailWriter = typeof(SmtpClient).Assembly.GetType("System.Net.Mail.MailWriter"); - public static readonly ConstructorInfo MailWriterConstructor = MailWriter.GetConstructor(PrivateInstanceFlags, null, new[] { typeof(Stream) }, null); - public static readonly MethodInfo CloseMethod = MailWriter.GetMethod("Close", PrivateInstanceFlags); - public static readonly MethodInfo SendMethod = typeof(MailMessage).GetMethod("Send", PrivateInstanceFlags); - public static readonly bool IsRunningInDotNetFourPointFive = SendMethod.GetParameters().Length == 3; - } - } + /// The caller. + /// A MemoryStream with the raw contents of this MailMessage. + public static MemoryStream ToMimeMessage(this MailMessage self) { + if(self == null) { + throw new ArgumentNullException(nameof(self)); + } + + MemoryStream result = new MemoryStream(); + Object mailWriter = MimeMessageConstants.MailWriterConstructor.Invoke(new Object[] { result }); + _ = MimeMessageConstants.SendMethod.Invoke( + self, + MimeMessageConstants.PrivateInstanceFlags, + null, + MimeMessageConstants.IsRunningInDotNetFourPointFive ? new[] { mailWriter, true, true } : new[] { mailWriter, true }, + null); + + result = new MemoryStream(result.ToArray()); + _ = MimeMessageConstants.CloseMethod.Invoke( + mailWriter, + MimeMessageConstants.PrivateInstanceFlags, + null, + new Object[] { }, + null); + result.Position = 0; + return result; + } + + internal static class MimeMessageConstants { + public static readonly BindingFlags PrivateInstanceFlags = BindingFlags.Instance | BindingFlags.NonPublic; + public static readonly Type MailWriter = typeof(SmtpClient).Assembly.GetType("System.Net.Mail.MailWriter"); + public static readonly ConstructorInfo MailWriterConstructor = MailWriter.GetConstructor(PrivateInstanceFlags, null, new[] { typeof(Stream) }, null); + public static readonly MethodInfo CloseMethod = MailWriter.GetMethod("Close", PrivateInstanceFlags); + public static readonly MethodInfo SendMethod = typeof(MailMessage).GetMethod("Send", PrivateInstanceFlags); + public static readonly Boolean IsRunningInDotNetFourPointFive = SendMethod.GetParameters().Length == 3; + } + } } #endif diff --git a/Unosquare.Swan/Extensions.Network.cs b/Unosquare.Swan/Extensions.Network.cs index 225a4f4..63063fa 100644 --- a/Unosquare.Swan/Extensions.Network.cs +++ b/Unosquare.Swan/Extensions.Network.cs @@ -1,58 +1,58 @@ -namespace Unosquare.Swan -{ - using System; - using System.Linq; - using System.Net; - using System.Net.Sockets; - +using System; +using System.Linq; +using System.Net; +using System.Net.Sockets; + +namespace Unosquare.Swan { + /// + /// Provides various extension methods for networking-related tasks. + /// + public static class NetworkExtensions { /// - /// Provides various extension methods for networking-related tasks. + /// Determines whether the IP address is private. /// - public static class NetworkExtensions - { - /// - /// Determines whether the IP address is private. - /// - /// The IP address. - /// - /// True if the IP Address is private; otherwise, false. - /// - /// address. - public static bool IsPrivateAddress(this IPAddress address) - { - if (address == null) - throw new ArgumentNullException(nameof(address)); - - var octets = address.ToString().Split(new[] { "." }, StringSplitOptions.RemoveEmptyEntries).Select(byte.Parse).ToArray(); - var is24Bit = octets[0] == 10; - var is20Bit = octets[0] == 172 && (octets[1] >= 16 && octets[1] <= 31); - var is16Bit = octets[0] == 192 && octets[1] == 168; - - return is24Bit || is20Bit || is16Bit; - } - - /// - /// Converts an IPv4 Address to its Unsigned, 32-bit integer representation. - /// - /// The address. - /// - /// A 32-bit unsigned integer converted from four bytes at a specified position in a byte array. - /// - /// address. - /// InterNetwork - address. - public static uint ToUInt32(this IPAddress address) - { - if (address == null) - throw new ArgumentNullException(nameof(address)); - - if (address.AddressFamily != AddressFamily.InterNetwork) - throw new ArgumentException($"Address has to be of family '{nameof(AddressFamily.InterNetwork)}'", nameof(address)); - - var addressBytes = address.GetAddressBytes(); - if (BitConverter.IsLittleEndian) - Array.Reverse(addressBytes); - - return BitConverter.ToUInt32(addressBytes, 0); - } - } + /// The IP address. + /// + /// True if the IP Address is private; otherwise, false. + /// + /// address. + public static Boolean IsPrivateAddress(this IPAddress address) { + if(address == null) { + throw new ArgumentNullException(nameof(address)); + } + + Byte[] octets = address.ToString().Split(new[] { "." }, StringSplitOptions.RemoveEmptyEntries).Select(Byte.Parse).ToArray(); + Boolean is24Bit = octets[0] == 10; + Boolean is20Bit = octets[0] == 172 && octets[1] >= 16 && octets[1] <= 31; + Boolean is16Bit = octets[0] == 192 && octets[1] == 168; + + return is24Bit || is20Bit || is16Bit; + } + + /// + /// Converts an IPv4 Address to its Unsigned, 32-bit integer representation. + /// + /// The address. + /// + /// A 32-bit unsigned integer converted from four bytes at a specified position in a byte array. + /// + /// address. + /// InterNetwork - address. + public static UInt32 ToUInt32(this IPAddress address) { + if(address == null) { + throw new ArgumentNullException(nameof(address)); + } + + if(address.AddressFamily != AddressFamily.InterNetwork) { + throw new ArgumentException($"Address has to be of family '{nameof(AddressFamily.InterNetwork)}'", nameof(address)); + } + + Byte[] addressBytes = address.GetAddressBytes(); + if(BitConverter.IsLittleEndian) { + Array.Reverse(addressBytes); + } + + return BitConverter.ToUInt32(addressBytes, 0); + } + } } diff --git a/Unosquare.Swan/Extensions.WindowsServices.cs b/Unosquare.Swan/Extensions.WindowsServices.cs index 9fb0fdb..f231cae 100644 --- a/Unosquare.Swan/Extensions.WindowsServices.cs +++ b/Unosquare.Swan/Extensions.WindowsServices.cs @@ -1,80 +1,75 @@ #if !NETSTANDARD1_3 -namespace Unosquare.Swan -{ - using System; - using System.Collections.Generic; - using System.Reflection; - using System.Threading; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; #if NET452 - using System.ServiceProcess; +using System.ServiceProcess; #else - using Abstractions; +using Abstractions; #endif - +namespace Unosquare.Swan { + /// + /// Extension methods. + /// + public static class WindowsServicesExtensions { /// - /// Extension methods. + /// Runs a service in console mode. /// - public static class WindowsServicesExtensions - { - /// - /// Runs a service in console mode. - /// - /// The service to run. - public static void RunInConsoleMode(this ServiceBase serviceToRun) - { - if (serviceToRun == null) - throw new ArgumentNullException(nameof(serviceToRun)); - - RunInConsoleMode(new[] { serviceToRun }); - } - - /// - /// Runs a set of services in console mode. - /// - /// The services to run. - public static void RunInConsoleMode(this ServiceBase[] servicesToRun) - { - if (servicesToRun == null) - throw new ArgumentNullException(nameof(servicesToRun)); - - const string onStartMethodName = "OnStart"; - const string onStopMethodName = "OnStop"; - - var onStartMethod = typeof(ServiceBase).GetMethod(onStartMethodName, - BindingFlags.Instance | BindingFlags.NonPublic); - var onStopMethod = typeof(ServiceBase).GetMethod(onStopMethodName, - BindingFlags.Instance | BindingFlags.NonPublic); - - var serviceThreads = new List(); - "Starting services . . .".Info(Runtime.EntryAssemblyName.Name); - - foreach (var service in servicesToRun) - { - var thread = new Thread(() => - { - onStartMethod.Invoke(service, new object[] { new string[] { } }); - $"Started service '{service.GetType().Name}'".Info(service.GetType()); - }); - - serviceThreads.Add(thread); - thread.Start(); - } - - "Press any key to stop all services.".Info(Runtime.EntryAssemblyName.Name); - Terminal.ReadKey(true, true); - "Stopping services . . .".Info(Runtime.EntryAssemblyName.Name); - - foreach (var service in servicesToRun) - { - onStopMethod.Invoke(service, null); - $"Stopped service '{service.GetType().Name}'".Info(service.GetType()); - } - - foreach (var thread in serviceThreads) - thread.Join(); - - "Stopped all services.".Info(Runtime.EntryAssemblyName.Name); - } - } + /// The service to run. + public static void RunInConsoleMode(this ServiceBase serviceToRun) { + if(serviceToRun == null) { + throw new ArgumentNullException(nameof(serviceToRun)); + } + + RunInConsoleMode(new[] { serviceToRun }); + } + + /// + /// Runs a set of services in console mode. + /// + /// The services to run. + public static void RunInConsoleMode(this ServiceBase[] servicesToRun) { + if(servicesToRun == null) { + throw new ArgumentNullException(nameof(servicesToRun)); + } + + const String onStartMethodName = "OnStart"; + const String onStopMethodName = "OnStop"; + + MethodInfo onStartMethod = typeof(ServiceBase).GetMethod(onStartMethodName, + BindingFlags.Instance | BindingFlags.NonPublic); + MethodInfo onStopMethod = typeof(ServiceBase).GetMethod(onStopMethodName, + BindingFlags.Instance | BindingFlags.NonPublic); + + List serviceThreads = new List(); + "Starting services . . .".Info(Runtime.EntryAssemblyName.Name); + + foreach(ServiceBase service in servicesToRun) { + Thread thread = new Thread(() => { + _ = onStartMethod.Invoke(service, new Object[] { new String[] { } }); + $"Started service '{service.GetType().Name}'".Info(service.GetType()); + }); + + serviceThreads.Add(thread); + thread.Start(); + } + + "Press any key to stop all services.".Info(Runtime.EntryAssemblyName.Name); + _ = Terminal.ReadKey(true, true); + "Stopping services . . .".Info(Runtime.EntryAssemblyName.Name); + + foreach(ServiceBase service in servicesToRun) { + _ = onStopMethod.Invoke(service, null); + $"Stopped service '{service.GetType().Name}'".Info(service.GetType()); + } + + foreach(Thread thread in serviceThreads) { + thread.Join(); + } + + "Stopped all services.".Info(Runtime.EntryAssemblyName.Name); + } + } } #endif \ No newline at end of file diff --git a/Unosquare.Swan/Formatters/BitmapBuffer.cs b/Unosquare.Swan/Formatters/BitmapBuffer.cs index 180a127..c0c9e5d 100644 --- a/Unosquare.Swan/Formatters/BitmapBuffer.cs +++ b/Unosquare.Swan/Formatters/BitmapBuffer.cs @@ -1,179 +1,184 @@ -#if NET452 -namespace Unosquare.Swan.Formatters -{ - using System; - using System.Drawing; - using System.Drawing.Imaging; - using System.Runtime.InteropServices; - using System.Threading.Tasks; +#if NET452 +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +namespace Unosquare.Swan.Formatters { + /// + /// Represents a buffer of bytes containing pixels in BGRA byte order + /// loaded from an image that is passed on to the constructor. + /// Data contains all the raw bytes (without scanline left-over bytes) + /// where they can be quickly changed and then a new bitmap + /// can be created from the byte data. + /// + public class BitmapBuffer { /// - /// Represents a buffer of bytes containing pixels in BGRA byte order - /// loaded from an image that is passed on to the constructor. - /// Data contains all the raw bytes (without scanline left-over bytes) - /// where they can be quickly changed and then a new bitmap - /// can be created from the byte data. + /// A constant representing the number of + /// bytes per pixel in the pixel data. This is + /// always 4 but it is kept here for readability. /// - public class BitmapBuffer - { - /// - /// A constant representing the number of - /// bytes per pixel in the pixel data. This is - /// always 4 but it is kept here for readability. - /// - public const int BytesPerPixel = 4; - - /// - /// The blue byte offset within a pixel offset. This is 0. - /// - public const int BOffset = 0; - - /// - /// The green byte offset within a pixel offset. This is 1. - /// - public const int GOffset = 1; - - /// - /// The red byte offset within a pixel offset. This is 2. - /// - public const int ROffset = 2; - - /// - /// The alpha byte offset within a pixel offset. This is 3. - /// - public const int AOffset = 3; - - /// - /// Initializes a new instance of the class. - /// Data will not contain left-over stride bytes - /// - /// The source image. - public BitmapBuffer(Image sourceImage) - { - // Acquire or create the source bitmap in a manageable format - var disposeSourceBitmap = false; - if (!(sourceImage is Bitmap sourceBitmap) || sourceBitmap.PixelFormat != PixelFormat) - { - sourceBitmap = new Bitmap(sourceImage.Width, sourceImage.Height, PixelFormat); - using (var g = Graphics.FromImage(sourceBitmap)) - { - g.DrawImage(sourceImage, 0, 0); - } - - // We created this bitmap. Make sure we clear it from memory - disposeSourceBitmap = true; - } - - // Lock the bits - var sourceDataLocker = sourceBitmap.LockBits( - new Rectangle(0, 0, sourceBitmap.Width, sourceBitmap.Height), - ImageLockMode.ReadOnly, - sourceBitmap.PixelFormat); - - // Set basic properties - ImageWidth = sourceBitmap.Width; - ImageHeight = sourceBitmap.Height; - LineStride = sourceDataLocker.Stride; - - // State variables - LineLength = sourceBitmap.Width * BytesPerPixel; // may or may not be equal to the Stride - Data = new byte[LineLength * sourceBitmap.Height]; - - // copy line by line in order to ignore the useless left-over stride - Parallel.For(0, sourceBitmap.Height, y => - { - var sourceAddress = sourceDataLocker.Scan0 + (sourceDataLocker.Stride * y); - var targetAddress = y * LineLength; - Marshal.Copy(sourceAddress, Data, targetAddress, LineLength); - }); - - // finally unlock the bitmap - sourceBitmap.UnlockBits(sourceDataLocker); - - // dispose the source bitmap if we had to create it - if (disposeSourceBitmap) - { - sourceBitmap.Dispose(); - } - } - - /// - /// Contains all the bytes of the pixel data - /// Each horizontal scanline is represented by LineLength - /// rather than by LinceStride. The left-over stride bytes - /// are removed. - /// - public byte[] Data { get; } - - /// - /// Gets the width of the image. - /// - public int ImageWidth { get; } - - /// - /// Gets the height of the image. - /// - public int ImageHeight { get; } - - /// - /// Gets the pixel format. This will always be Format32bppArgb. - /// - public PixelFormat PixelFormat { get; } = PixelFormat.Format32bppArgb; - - /// - /// Gets the length in bytes of a line of pixel data. - /// Basically the same as Line Length except Stride might be a little larger as - /// some bitmaps might be DWORD-algned. - /// - public int LineStride { get; } - - /// - /// Gets the length in bytes of a line of pixel data. - /// Basically the same as Stride except Stride might be a little larger as - /// some bitmaps might be DWORD-algned. - /// - public int LineLength { get; } - - /// - /// Gets the index of the first byte in the BGRA pixel data for the given image coordinates. - /// - /// The x. - /// The y. - /// Index of the first byte in the BGRA pixel. - /// - /// x - /// or - /// y. - /// - public int GetPixelOffset(int x, int y) - { - if (x < 0 || x > ImageWidth) throw new ArgumentOutOfRangeException(nameof(x)); - if (y < 0 || y > ImageHeight) throw new ArgumentOutOfRangeException(nameof(y)); - - return (y * LineLength) + (x * BytesPerPixel); - } - - /// - /// Converts the pixel data bytes held in the buffer - /// to a 32-bit RGBA bitmap. - /// - /// Pixel data for a graphics image and its attribute. - public Bitmap ToBitmap() - { - var bitmap = new Bitmap(ImageWidth, ImageHeight, PixelFormat); - var bitLocker = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.WriteOnly, bitmap.PixelFormat); - - Parallel.For(0, bitmap.Height, y => - { - var sourceOffset = GetPixelOffset(0, y); - var targetOffset = bitLocker.Scan0 + (y * bitLocker.Stride); - Marshal.Copy(Data, sourceOffset, targetOffset, bitLocker.Width); - }); - - bitmap.UnlockBits(bitLocker); - - return bitmap; - } - } + public const Int32 BytesPerPixel = 4; + + /// + /// The blue byte offset within a pixel offset. This is 0. + /// + public const Int32 BOffset = 0; + + /// + /// The green byte offset within a pixel offset. This is 1. + /// + public const Int32 GOffset = 1; + + /// + /// The red byte offset within a pixel offset. This is 2. + /// + public const Int32 ROffset = 2; + + /// + /// The alpha byte offset within a pixel offset. This is 3. + /// + public const Int32 AOffset = 3; + + /// + /// Initializes a new instance of the class. + /// Data will not contain left-over stride bytes + /// + /// The source image. + public BitmapBuffer(Image sourceImage) { + // Acquire or create the source bitmap in a manageable format + Boolean disposeSourceBitmap = false; + if(!(sourceImage is Bitmap sourceBitmap) || sourceBitmap.PixelFormat != this.PixelFormat) { + sourceBitmap = new Bitmap(sourceImage.Width, sourceImage.Height, this.PixelFormat); + using(Graphics g = Graphics.FromImage(sourceBitmap)) { + g.DrawImage(sourceImage, 0, 0); + } + + // We created this bitmap. Make sure we clear it from memory + disposeSourceBitmap = true; + } + + // Lock the bits + BitmapData sourceDataLocker = sourceBitmap.LockBits( + new Rectangle(0, 0, sourceBitmap.Width, sourceBitmap.Height), + ImageLockMode.ReadOnly, + sourceBitmap.PixelFormat); + + // Set basic properties + this.ImageWidth = sourceBitmap.Width; + this.ImageHeight = sourceBitmap.Height; + this.LineStride = sourceDataLocker.Stride; + + // State variables + this.LineLength = sourceBitmap.Width * BytesPerPixel; // may or may not be equal to the Stride + this.Data = new Byte[this.LineLength * sourceBitmap.Height]; + + // copy line by line in order to ignore the useless left-over stride + _ = Parallel.For(0, sourceBitmap.Height, y => { + IntPtr sourceAddress = sourceDataLocker.Scan0 + sourceDataLocker.Stride * y; + Int32 targetAddress = y * this.LineLength; + Marshal.Copy(sourceAddress, this.Data, targetAddress, this.LineLength); + }); + + // finally unlock the bitmap + sourceBitmap.UnlockBits(sourceDataLocker); + + // dispose the source bitmap if we had to create it + if(disposeSourceBitmap) { + sourceBitmap.Dispose(); + } + } + + /// + /// Contains all the bytes of the pixel data + /// Each horizontal scanline is represented by LineLength + /// rather than by LinceStride. The left-over stride bytes + /// are removed. + /// + public Byte[] Data { + get; + } + + /// + /// Gets the width of the image. + /// + public Int32 ImageWidth { + get; + } + + /// + /// Gets the height of the image. + /// + public Int32 ImageHeight { + get; + } + + /// + /// Gets the pixel format. This will always be Format32bppArgb. + /// + public PixelFormat PixelFormat { get; } = PixelFormat.Format32bppArgb; + + /// + /// Gets the length in bytes of a line of pixel data. + /// Basically the same as Line Length except Stride might be a little larger as + /// some bitmaps might be DWORD-algned. + /// + public Int32 LineStride { + get; + } + + /// + /// Gets the length in bytes of a line of pixel data. + /// Basically the same as Stride except Stride might be a little larger as + /// some bitmaps might be DWORD-algned. + /// + public Int32 LineLength { + get; + } + + /// + /// Gets the index of the first byte in the BGRA pixel data for the given image coordinates. + /// + /// The x. + /// The y. + /// Index of the first byte in the BGRA pixel. + /// + /// x + /// or + /// y. + /// + public Int32 GetPixelOffset(Int32 x, Int32 y) { + if(x < 0 || x > this.ImageWidth) { + throw new ArgumentOutOfRangeException(nameof(x)); + } + + if(y < 0 || y > this.ImageHeight) { + throw new ArgumentOutOfRangeException(nameof(y)); + } + + return y * this.LineLength + x * BytesPerPixel; + } + + /// + /// Converts the pixel data bytes held in the buffer + /// to a 32-bit RGBA bitmap. + /// + /// Pixel data for a graphics image and its attribute. + public Bitmap ToBitmap() { + Bitmap bitmap = new Bitmap(this.ImageWidth, this.ImageHeight, this.PixelFormat); + BitmapData bitLocker = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.WriteOnly, bitmap.PixelFormat); + + _ = Parallel.For(0, bitmap.Height, y => { + Int32 sourceOffset = this.GetPixelOffset(0, y); + IntPtr targetOffset = bitLocker.Scan0 + y * bitLocker.Stride; + Marshal.Copy(this.Data, sourceOffset, targetOffset, bitLocker.Width); + }); + + bitmap.UnlockBits(bitLocker); + + return bitmap; + } + } } #endif \ No newline at end of file diff --git a/Unosquare.Swan/Models/OkOrError.cs b/Unosquare.Swan/Models/OkOrError.cs index dbc1cac..6efbd58 100644 --- a/Unosquare.Swan/Models/OkOrError.cs +++ b/Unosquare.Swan/Models/OkOrError.cs @@ -1,48 +1,52 @@ -namespace Unosquare.Swan.Models -{ +using System; + +namespace Unosquare.Swan.Models { + /// + /// Represents a Ok value or Error value. + /// + /// The type of OK value. + /// The type of the error. + public class OkOrError { /// - /// Represents a Ok value or Error value. + /// Gets or sets a value indicating whether this instance is Ok. /// - /// The type of OK value. - /// The type of the error. - public class OkOrError - { - /// - /// Gets or sets a value indicating whether this instance is Ok. - /// - /// - /// true if this instance is ok; otherwise, false. - /// - public bool IsOk => !Equals(Ok, default(T)); - - /// - /// Gets or sets the ok. - /// - /// - /// The ok. - /// - public T Ok { get; set; } - - /// - /// Gets or sets the error. - /// - /// - /// The error. - /// - public TError Error { get; set; } - - /// - /// Creates a new OkOrError from the specified Ok object. - /// - /// The ok. - /// OkOrError instance. - public static OkOrError FromOk(T ok) => new OkOrError { Ok = ok }; - - /// - /// Creates a new OkOrError from the specified Error object. - /// - /// The error. - /// OkOrError instance. - public static OkOrError FromError(TError error) => new OkOrError { Error = error }; - } + /// + /// true if this instance is ok; otherwise, false. + /// + public Boolean IsOk => !Equals(this.Ok, default(T)); + + /// + /// Gets or sets the ok. + /// + /// + /// The ok. + /// + public T Ok { + get; set; + } + + /// + /// Gets or sets the error. + /// + /// + /// The error. + /// + public TError Error { + get; set; + } + + /// + /// Creates a new OkOrError from the specified Ok object. + /// + /// The ok. + /// OkOrError instance. + public static OkOrError FromOk(T ok) => new OkOrError { Ok = ok }; + + /// + /// Creates a new OkOrError from the specified Error object. + /// + /// The error. + /// OkOrError instance. + public static OkOrError FromError(TError error) => new OkOrError { Error = error }; + } } diff --git a/Unosquare.Swan/Network.cs b/Unosquare.Swan/Network.cs index 945e114..743c6b1 100644 --- a/Unosquare.Swan/Network.cs +++ b/Unosquare.Swan/Network.cs @@ -1,450 +1,414 @@ -namespace Unosquare.Swan -{ - using Networking; - using System; - using System.Collections.Generic; - using System.Linq; - using System.Net; - using System.Net.Http; - using System.Net.NetworkInformation; - using System.Net.Sockets; - using System.Threading; - using System.Threading.Tasks; - +using Unosquare.Swan.Networking; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace Unosquare.Swan { + /// + /// Provides miscellaneous network utilities such as a Public IP finder, + /// a DNS client to query DNS records of any kind, and an NTP client. + /// + public static class Network { /// - /// Provides miscellaneous network utilities such as a Public IP finder, - /// a DNS client to query DNS records of any kind, and an NTP client. + /// The DNS default port. /// - public static class Network - { - /// - /// The DNS default port. - /// - public const int DnsDefaultPort = 53; - - /// - /// The NTP default port. - /// - public const int NtpDefaultPort = 123; - - /// - /// Gets the name of the host. - /// - /// - /// The name of the host. - /// - public static string HostName => IPGlobalProperties.GetIPGlobalProperties().HostName; - - /// - /// Gets the name of the network domain. - /// - /// - /// The name of the network domain. - /// - public static string DomainName => IPGlobalProperties.GetIPGlobalProperties().DomainName; - - #region IP Addresses and Adapters Information Methods - - /// - /// Gets the active IPv4 interfaces. - /// Only those interfaces with a valid unicast address and a valid gateway will be returned in the collection. - /// - /// - /// A collection of NetworkInterface/IPInterfaceProperties pairs - /// that represents the active IPv4 interfaces. - /// - public static Dictionary GetIPv4Interfaces() - { - // zero conf ip address - var zeroConf = new IPAddress(0); - - var adapters = NetworkInterface.GetAllNetworkInterfaces() - .Where(network => - network.OperationalStatus == OperationalStatus.Up - && network.NetworkInterfaceType != NetworkInterfaceType.Unknown - && network.NetworkInterfaceType != NetworkInterfaceType.Loopback) - .ToArray(); - - var result = new Dictionary(); - - foreach (var adapter in adapters) - { - var properties = adapter.GetIPProperties(); - if (properties == null - || properties.GatewayAddresses.Count == 0 - || properties.GatewayAddresses.All(gateway => Equals(gateway.Address, zeroConf)) - || properties.UnicastAddresses.Count == 0 - || properties.GatewayAddresses.All(address => Equals(address.Address, zeroConf)) - || properties.UnicastAddresses.Any(a => a.Address.AddressFamily == AddressFamily.InterNetwork) == - false) - continue; - - result[adapter] = properties; - } - - return result; - } - - /// - /// Retrieves the local ip addresses. - /// - /// if set to true [include loopback]. - /// An array of local ip addresses. - public static IPAddress[] GetIPv4Addresses(bool includeLoopback = true) => - GetIPv4Addresses(NetworkInterfaceType.Unknown, true, includeLoopback); - - /// - /// Retrieves the local ip addresses. - /// - /// Type of the interface. - /// if set to true [skip type filter]. - /// if set to true [include loopback]. - /// An array of local ip addresses. - public static IPAddress[] GetIPv4Addresses( - NetworkInterfaceType interfaceType, - bool skipTypeFilter = false, - bool includeLoopback = false) - { - var addressList = new List(); - var interfaces = NetworkInterface.GetAllNetworkInterfaces() - .Where(ni => + public const Int32 DnsDefaultPort = 53; + + /// + /// The NTP default port. + /// + public const Int32 NtpDefaultPort = 123; + + /// + /// Gets the name of the host. + /// + /// + /// The name of the host. + /// + public static String HostName => IPGlobalProperties.GetIPGlobalProperties().HostName; + + /// + /// Gets the name of the network domain. + /// + /// + /// The name of the network domain. + /// + public static String DomainName => IPGlobalProperties.GetIPGlobalProperties().DomainName; + + #region IP Addresses and Adapters Information Methods + + /// + /// Gets the active IPv4 interfaces. + /// Only those interfaces with a valid unicast address and a valid gateway will be returned in the collection. + /// + /// + /// A collection of NetworkInterface/IPInterfaceProperties pairs + /// that represents the active IPv4 interfaces. + /// + public static Dictionary GetIPv4Interfaces() { + // zero conf ip address + IPAddress zeroConf = new IPAddress(0); + + NetworkInterface[] adapters = NetworkInterface.GetAllNetworkInterfaces() + .Where(network => + network.OperationalStatus == OperationalStatus.Up + && network.NetworkInterfaceType != NetworkInterfaceType.Unknown + && network.NetworkInterfaceType != NetworkInterfaceType.Loopback) + .ToArray(); + + Dictionary result = new Dictionary(); + + foreach(NetworkInterface adapter in adapters) { + IPInterfaceProperties properties = adapter.GetIPProperties(); + if(properties == null + || properties.GatewayAddresses.Count == 0 + || properties.GatewayAddresses.All(gateway => Equals(gateway.Address, zeroConf)) + || properties.UnicastAddresses.Count == 0 + || properties.GatewayAddresses.All(address => Equals(address.Address, zeroConf)) + || properties.UnicastAddresses.Any(a => a.Address.AddressFamily == AddressFamily.InterNetwork) == + false) { + continue; + } + + result[adapter] = properties; + } + + return result; + } + + /// + /// Retrieves the local ip addresses. + /// + /// if set to true [include loopback]. + /// An array of local ip addresses. + public static IPAddress[] GetIPv4Addresses(Boolean includeLoopback = true) => + GetIPv4Addresses(NetworkInterfaceType.Unknown, true, includeLoopback); + + /// + /// Retrieves the local ip addresses. + /// + /// Type of the interface. + /// if set to true [skip type filter]. + /// if set to true [include loopback]. + /// An array of local ip addresses. + public static IPAddress[] GetIPv4Addresses( + NetworkInterfaceType interfaceType, + Boolean skipTypeFilter = false, + Boolean includeLoopback = false) { + List addressList = new List(); + NetworkInterface[] interfaces = NetworkInterface.GetAllNetworkInterfaces() + .Where(ni => #if NET452 - ni.IsReceiveOnly == false && + ni.IsReceiveOnly == false && #endif - (skipTypeFilter || ni.NetworkInterfaceType == interfaceType) && - ni.OperationalStatus == OperationalStatus.Up) - .ToArray(); - - foreach (var networkInterface in interfaces) - { - var properties = networkInterface.GetIPProperties(); - - if (properties.GatewayAddresses.All(g => g.Address.AddressFamily != AddressFamily.InterNetwork)) - continue; - - addressList.AddRange(properties.UnicastAddresses - .Where(i => i.Address.AddressFamily == AddressFamily.InterNetwork) - .Select(i => i.Address)); - } - - if (includeLoopback || interfaceType == NetworkInterfaceType.Loopback) - addressList.Add(IPAddress.Loopback); - - return addressList.ToArray(); - } - - /// - /// Gets the public IP address using ipify.org. - /// - /// The cancellation token. - /// A public IP address of the result produced by this Task. - public static async Task GetPublicIPAddressAsync(CancellationToken ct = default) - { - using (var client = new HttpClient()) - { - var response = await client.GetAsync("https://api.ipify.org", ct).ConfigureAwait(false); - return IPAddress.Parse(await response.Content.ReadAsStringAsync().ConfigureAwait(false)); - } - } - - /// - /// Gets the public IP address using ipify.org. - /// - /// A public ip address. - public static IPAddress GetPublicIPAddress() => GetPublicIPAddressAsync().GetAwaiter().GetResult(); - - /// - /// Gets the configured IPv4 DNS servers for the active network interfaces. - /// - /// - /// A collection of NetworkInterface/IPInterfaceProperties pairs - /// that represents the active IPv4 interfaces. - /// - public static IPAddress[] GetIPv4DnsServers() - => GetIPv4Interfaces() - .Select(a => a.Value.DnsAddresses.Where(d => d.AddressFamily == AddressFamily.InterNetwork)) - .SelectMany(d => d) - .ToArray(); - - #endregion - - #region DNS and NTP Clients - - /// - /// Gets the DNS host entry (a list of IP addresses) for the domain name. - /// - /// The FQDN. - /// An array of local ip addresses. - public static IPAddress[] GetDnsHostEntry(string fqdn) - { - var dnsServer = GetIPv4DnsServers().FirstOrDefault() ?? IPAddress.Parse("8.8.8.8"); - return GetDnsHostEntry(fqdn, dnsServer, DnsDefaultPort); - } - - /// - /// Gets the DNS host entry (a list of IP addresses) for the domain name. - /// - /// The FQDN. - /// The cancellation token. - /// An array of local ip addresses of the result produced by this task. - public static Task GetDnsHostEntryAsync(string fqdn, - CancellationToken ct = default) - { - return Task.Run(() => GetDnsHostEntry(fqdn), ct); - } - - /// - /// Gets the DNS host entry (a list of IP addresses) for the domain name. - /// - /// The FQDN. - /// The DNS server. - /// The port. - /// - /// An array of local ip addresses. - /// - /// fqdn. - public static IPAddress[] GetDnsHostEntry(string fqdn, IPAddress dnsServer, int port) - { - if (fqdn == null) - throw new ArgumentNullException(nameof(fqdn)); - - if (fqdn.IndexOf(".", StringComparison.Ordinal) == -1) - { - fqdn += "." + IPGlobalProperties.GetIPGlobalProperties().DomainName; - } - - while (true) - { - if (fqdn.EndsWith(".") == false) break; - - fqdn = fqdn.Substring(0, fqdn.Length - 1); - } - - var client = new DnsClient(dnsServer, port); - var result = client.Lookup(fqdn); - return result.ToArray(); - } - - /// - /// Gets the DNS host entry (a list of IP addresses) for the domain name. - /// - /// The FQDN. - /// The DNS server. - /// The port. - /// The cancellation token. - /// An array of local ip addresses of the result produced by this task. - public static Task GetDnsHostEntryAsync( - string fqdn, - IPAddress dnsServer, - int port, - CancellationToken ct = default) - { - return Task.Run(() => GetDnsHostEntry(fqdn, dnsServer, port), ct); - } - - /// - /// Gets the reverse lookup FQDN of the given IP Address. - /// - /// The query. - /// The DNS server. - /// The port. - /// A that represents the current object. - public static string GetDnsPointerEntry(IPAddress query, IPAddress dnsServer, int port) => new DnsClient(dnsServer, port).Reverse(query); - - /// - /// Gets the reverse lookup FQDN of the given IP Address. - /// - /// The query. - /// The DNS server. - /// The port. - /// The cancellation token. - /// A that represents the current object. - public static Task GetDnsPointerEntryAsync( - IPAddress query, - IPAddress dnsServer, - int port, - CancellationToken ct = default) - { - return Task.Run(() => GetDnsPointerEntry(query, dnsServer, port), ct); - } - - /// - /// Gets the reverse lookup FQDN of the given IP Address. - /// - /// The query. - /// A that represents the current object. - public static string GetDnsPointerEntry(IPAddress query) => new DnsClient(GetIPv4DnsServers().FirstOrDefault()).Reverse(query); - - /// - /// Gets the reverse lookup FQDN of the given IP Address. - /// - /// The query. - /// The cancellation token. - /// A that represents the current object. - public static Task GetDnsPointerEntryAsync( - IPAddress query, - CancellationToken ct = default) - { - return Task.Run(() => GetDnsPointerEntry(query), ct); - } - - /// - /// Queries the DNS server for the specified record type. - /// - /// The query. - /// Type of the record. - /// The DNS server. - /// The port. - /// - /// Appropriate DNS server for the specified record type. - /// - public static DnsQueryResult QueryDns(string query, DnsRecordType recordType, IPAddress dnsServer, int port) - { - if (query == null) - throw new ArgumentNullException(nameof(query)); - - var response = new DnsClient(dnsServer, port).Resolve(query, recordType); - return new DnsQueryResult(response); - } - - /// - /// Queries the DNS server for the specified record type. - /// - /// The query. - /// Type of the record. - /// The DNS server. - /// The port. - /// The cancellation token. - /// Queries the DNS server for the specified record type of the result produced by this Task. - public static Task QueryDnsAsync( - string query, - DnsRecordType recordType, - IPAddress dnsServer, - int port, - CancellationToken ct = default) - { - return Task.Run(() => QueryDns(query, recordType, dnsServer, port), ct); - } - - /// - /// Queries the DNS server for the specified record type. - /// - /// The query. - /// Type of the record. - /// Appropriate DNS server for the specified record type. - public static DnsQueryResult QueryDns(string query, DnsRecordType recordType) => QueryDns(query, recordType, GetIPv4DnsServers().FirstOrDefault(), DnsDefaultPort); - - /// - /// Queries the DNS server for the specified record type. - /// - /// The query. - /// Type of the record. - /// The cancellation token. - /// Queries the DNS server for the specified record type of the result produced by this Task. - public static Task QueryDnsAsync( - string query, - DnsRecordType recordType, - CancellationToken ct = default) - { - return Task.Run(() => QueryDns(query, recordType), ct); - } - - /// - /// Gets the UTC time by querying from an NTP server. - /// - /// The NTP server address. - /// The port. - /// - /// A new instance of the DateTime structure to - /// the specified year, month, day, hour, minute and second. - /// - public static DateTime GetNetworkTimeUtc(IPAddress ntpServerAddress, int port = NtpDefaultPort) - { - if (ntpServerAddress == null) - throw new ArgumentNullException(nameof(ntpServerAddress)); - - // NTP message size - 16 bytes of the digest (RFC 2030) - var ntpData = new byte[48]; - - // Setting the Leap Indicator, Version Number and Mode values - ntpData[0] = 0x1B; // LI = 0 (no warning), VN = 3 (IPv4 only), Mode = 3 (Client Mode) - - // The UDP port number assigned to NTP is 123 - var endPoint = new IPEndPoint(ntpServerAddress, port); - var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - - socket.Connect(endPoint); - socket.ReceiveTimeout = 3000; // Stops code hang if NTP is blocked - socket.Send(ntpData); - socket.Receive(ntpData); - socket.Dispose(); - - // Offset to get to the "Transmit Timestamp" field (time at which the reply - // departed the server for the client, in 64-bit timestamp format." - const byte serverReplyTime = 40; - - // Get the seconds part - ulong intPart = BitConverter.ToUInt32(ntpData, serverReplyTime); - - // Get the seconds fraction - ulong fractPart = BitConverter.ToUInt32(ntpData, serverReplyTime + 4); - - // Convert From big-endian to little-endian to match the platform - if (BitConverter.IsLittleEndian) - { - intPart = intPart.SwapEndianness(); - fractPart = intPart.SwapEndianness(); - } - - var milliseconds = (intPart * 1000) + ((fractPart * 1000) / 0x100000000L); - - // The time is given in UTC - return new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds((long) milliseconds); - } - - /// - /// Gets the UTC time by querying from an NTP server. - /// - /// The NTP server, by default pool.ntp.org. - /// The port, by default NTP 123. - /// The UTC time by querying from an NTP server. - public static DateTime GetNetworkTimeUtc(string ntpServerName = "pool.ntp.org", - int port = NtpDefaultPort) - { - var addresses = GetDnsHostEntry(ntpServerName); - return GetNetworkTimeUtc(addresses.First(), port); - } - - /// - /// Gets the UTC time by querying from an NTP server. - /// - /// The NTP server address. - /// The port. - /// The cancellation token. - /// The UTC time by querying from an NTP server of the result produced by this Task. - public static Task GetNetworkTimeUtcAsync( - IPAddress ntpServerAddress, - int port = NtpDefaultPort, - CancellationToken ct = default) - { - return Task.Run(() => GetNetworkTimeUtc(ntpServerAddress, port), ct); - } - - /// - /// Gets the UTC time by querying from an NTP server. - /// - /// Name of the NTP server. - /// The port. - /// The cancellation token. - /// The UTC time by querying from an NTP server of the result produced by this Task. - public static Task GetNetworkTimeUtcAsync( - string ntpServerName = "pool.ntp.org", - int port = NtpDefaultPort, - CancellationToken ct = default) - { - return Task.Run(() => GetNetworkTimeUtc(ntpServerName, port), ct); - } - - #endregion - } + (skipTypeFilter || ni.NetworkInterfaceType == interfaceType) && + ni.OperationalStatus == OperationalStatus.Up) + .ToArray(); + + foreach(NetworkInterface networkInterface in interfaces) { + IPInterfaceProperties properties = networkInterface.GetIPProperties(); + + if(properties.GatewayAddresses.All(g => g.Address.AddressFamily != AddressFamily.InterNetwork)) { + continue; + } + + addressList.AddRange(properties.UnicastAddresses + .Where(i => i.Address.AddressFamily == AddressFamily.InterNetwork) + .Select(i => i.Address)); + } + + if(includeLoopback || interfaceType == NetworkInterfaceType.Loopback) { + addressList.Add(IPAddress.Loopback); + } + + return addressList.ToArray(); + } + + /// + /// Gets the public IP address using ipify.org. + /// + /// The cancellation token. + /// A public IP address of the result produced by this Task. + public static async Task GetPublicIPAddressAsync(CancellationToken ct = default) { + using(HttpClient client = new HttpClient()) { + HttpResponseMessage response = await client.GetAsync("https://api.ipify.org", ct).ConfigureAwait(false); + return IPAddress.Parse(await response.Content.ReadAsStringAsync().ConfigureAwait(false)); + } + } + + /// + /// Gets the public IP address using ipify.org. + /// + /// A public ip address. + public static IPAddress GetPublicIPAddress() => GetPublicIPAddressAsync().GetAwaiter().GetResult(); + + /// + /// Gets the configured IPv4 DNS servers for the active network interfaces. + /// + /// + /// A collection of NetworkInterface/IPInterfaceProperties pairs + /// that represents the active IPv4 interfaces. + /// + public static IPAddress[] GetIPv4DnsServers() + => GetIPv4Interfaces() + .Select(a => a.Value.DnsAddresses.Where(d => d.AddressFamily == AddressFamily.InterNetwork)) + .SelectMany(d => d) + .ToArray(); + + #endregion + + #region DNS and NTP Clients + + /// + /// Gets the DNS host entry (a list of IP addresses) for the domain name. + /// + /// The FQDN. + /// An array of local ip addresses. + public static IPAddress[] GetDnsHostEntry(String fqdn) { + IPAddress dnsServer = GetIPv4DnsServers().FirstOrDefault() ?? IPAddress.Parse("8.8.8.8"); + return GetDnsHostEntry(fqdn, dnsServer, DnsDefaultPort); + } + + /// + /// Gets the DNS host entry (a list of IP addresses) for the domain name. + /// + /// The FQDN. + /// The cancellation token. + /// An array of local ip addresses of the result produced by this task. + public static Task GetDnsHostEntryAsync(String fqdn, + CancellationToken ct = default) => Task.Run(() => GetDnsHostEntry(fqdn), ct); + + /// + /// Gets the DNS host entry (a list of IP addresses) for the domain name. + /// + /// The FQDN. + /// The DNS server. + /// The port. + /// + /// An array of local ip addresses. + /// + /// fqdn. + public static IPAddress[] GetDnsHostEntry(String fqdn, IPAddress dnsServer, Int32 port) { + if(fqdn == null) { + throw new ArgumentNullException(nameof(fqdn)); + } + + if(fqdn.IndexOf(".", StringComparison.Ordinal) == -1) { + fqdn += "." + IPGlobalProperties.GetIPGlobalProperties().DomainName; + } + + while(true) { + if(fqdn.EndsWith(".") == false) { + break; + } + + fqdn = fqdn.Substring(0, fqdn.Length - 1); + } + + DnsClient client = new DnsClient(dnsServer, port); + IList result = client.Lookup(fqdn); + return result.ToArray(); + } + + /// + /// Gets the DNS host entry (a list of IP addresses) for the domain name. + /// + /// The FQDN. + /// The DNS server. + /// The port. + /// The cancellation token. + /// An array of local ip addresses of the result produced by this task. + public static Task GetDnsHostEntryAsync(String fqdn, IPAddress dnsServer, Int32 port, CancellationToken ct = default) => Task.Run(() => GetDnsHostEntry(fqdn, dnsServer, port), ct); + + /// + /// Gets the reverse lookup FQDN of the given IP Address. + /// + /// The query. + /// The DNS server. + /// The port. + /// A that represents the current object. + public static String GetDnsPointerEntry(IPAddress query, IPAddress dnsServer, Int32 port) => new DnsClient(dnsServer, port).Reverse(query); + + /// + /// Gets the reverse lookup FQDN of the given IP Address. + /// + /// The query. + /// The DNS server. + /// The port. + /// The cancellation token. + /// A that represents the current object. + public static Task GetDnsPointerEntryAsync( + IPAddress query, + IPAddress dnsServer, + Int32 port, + CancellationToken ct = default) => Task.Run(() => GetDnsPointerEntry(query, dnsServer, port), ct); + + /// + /// Gets the reverse lookup FQDN of the given IP Address. + /// + /// The query. + /// A that represents the current object. + public static String GetDnsPointerEntry(IPAddress query) => new DnsClient(GetIPv4DnsServers().FirstOrDefault()).Reverse(query); + + /// + /// Gets the reverse lookup FQDN of the given IP Address. + /// + /// The query. + /// The cancellation token. + /// A that represents the current object. + public static Task GetDnsPointerEntryAsync( + IPAddress query, + CancellationToken ct = default) => Task.Run(() => GetDnsPointerEntry(query), ct); + + /// + /// Queries the DNS server for the specified record type. + /// + /// The query. + /// Type of the record. + /// The DNS server. + /// The port. + /// + /// Appropriate DNS server for the specified record type. + /// + public static DnsQueryResult QueryDns(String query, DnsRecordType recordType, IPAddress dnsServer, Int32 port) { + if(query == null) { + throw new ArgumentNullException(nameof(query)); + } + + DnsClient.DnsClientResponse response = new DnsClient(dnsServer, port).Resolve(query, recordType); + return new DnsQueryResult(response); + } + + /// + /// Queries the DNS server for the specified record type. + /// + /// The query. + /// Type of the record. + /// The DNS server. + /// The port. + /// The cancellation token. + /// Queries the DNS server for the specified record type of the result produced by this Task. + public static Task QueryDnsAsync( + String query, + DnsRecordType recordType, + IPAddress dnsServer, + Int32 port, + CancellationToken ct = default) => Task.Run(() => QueryDns(query, recordType, dnsServer, port), ct); + + /// + /// Queries the DNS server for the specified record type. + /// + /// The query. + /// Type of the record. + /// Appropriate DNS server for the specified record type. + public static DnsQueryResult QueryDns(String query, DnsRecordType recordType) => QueryDns(query, recordType, GetIPv4DnsServers().FirstOrDefault(), DnsDefaultPort); + + /// + /// Queries the DNS server for the specified record type. + /// + /// The query. + /// Type of the record. + /// The cancellation token. + /// Queries the DNS server for the specified record type of the result produced by this Task. + public static Task QueryDnsAsync( + String query, + DnsRecordType recordType, + CancellationToken ct = default) => Task.Run(() => QueryDns(query, recordType), ct); + + /// + /// Gets the UTC time by querying from an NTP server. + /// + /// The NTP server address. + /// The port. + /// + /// A new instance of the DateTime structure to + /// the specified year, month, day, hour, minute and second. + /// + public static DateTime GetNetworkTimeUtc(IPAddress ntpServerAddress, Int32 port = NtpDefaultPort) { + if(ntpServerAddress == null) { + throw new ArgumentNullException(nameof(ntpServerAddress)); + } + + // NTP message size - 16 bytes of the digest (RFC 2030) + Byte[] ntpData = new Byte[48]; + + // Setting the Leap Indicator, Version Number and Mode values + ntpData[0] = 0x1B; // LI = 0 (no warning), VN = 3 (IPv4 only), Mode = 3 (Client Mode) + + // The UDP port number assigned to NTP is 123 + IPEndPoint endPoint = new IPEndPoint(ntpServerAddress, port); + Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + + socket.Connect(endPoint); + socket.ReceiveTimeout = 3000; // Stops code hang if NTP is blocked + _ = socket.Send(ntpData); + _ = socket.Receive(ntpData); + socket.Dispose(); + + // Offset to get to the "Transmit Timestamp" field (time at which the reply + // departed the server for the client, in 64-bit timestamp format." + const Byte serverReplyTime = 40; + + // Get the seconds part + UInt64 intPart = BitConverter.ToUInt32(ntpData, serverReplyTime); + + // Get the seconds fraction + UInt64 fractPart = BitConverter.ToUInt32(ntpData, serverReplyTime + 4); + + // Convert From big-endian to little-endian to match the platform + if(BitConverter.IsLittleEndian) { + intPart = intPart.SwapEndianness(); + fractPart = intPart.SwapEndianness(); + } + + UInt64 milliseconds = intPart * 1000 + fractPart * 1000 / 0x100000000L; + + // The time is given in UTC + return new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds((Int64)milliseconds); + } + + /// + /// Gets the UTC time by querying from an NTP server. + /// + /// The NTP server, by default pool.ntp.org. + /// The port, by default NTP 123. + /// The UTC time by querying from an NTP server. + public static DateTime GetNetworkTimeUtc(String ntpServerName = "pool.ntp.org", + Int32 port = NtpDefaultPort) { + IPAddress[] addresses = GetDnsHostEntry(ntpServerName); + return GetNetworkTimeUtc(addresses.First(), port); + } + + /// + /// Gets the UTC time by querying from an NTP server. + /// + /// The NTP server address. + /// The port. + /// The cancellation token. + /// The UTC time by querying from an NTP server of the result produced by this Task. + public static Task GetNetworkTimeUtcAsync( + IPAddress ntpServerAddress, + Int32 port = NtpDefaultPort, + CancellationToken ct = default) => Task.Run(() => GetNetworkTimeUtc(ntpServerAddress, port), ct); + + /// + /// Gets the UTC time by querying from an NTP server. + /// + /// Name of the NTP server. + /// The port. + /// The cancellation token. + /// The UTC time by querying from an NTP server of the result produced by this Task. + public static Task GetNetworkTimeUtcAsync( + String ntpServerName = "pool.ntp.org", + Int32 port = NtpDefaultPort, + CancellationToken ct = default) => Task.Run(() => GetNetworkTimeUtc(ntpServerName, port), ct); + + #endregion + } } diff --git a/Unosquare.Swan/Networking/Connection.cs b/Unosquare.Swan/Networking/Connection.cs index f30b42e..3309ab1 100644 --- a/Unosquare.Swan/Networking/Connection.cs +++ b/Unosquare.Swan/Networking/Connection.cs @@ -1,892 +1,866 @@ -namespace Unosquare.Swan.Networking -{ - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Net; - using System.Net.Security; - using System.Net.Sockets; - using System.Security.Cryptography.X509Certificates; - using System.Text; - using System.Threading; - using System.Threading.Tasks; - +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Unosquare.Swan.Networking { + /// + /// Represents a network connection either on the server or on the client. It wraps a TcpClient + /// and its corresponding network streams. It is capable of working in 2 modes. Typically on the server side + /// you will need to enable continuous reading and events. On the client side you may want to disable continuous reading + /// and use the Read methods available. In continuous reading mode Read methods are not available and will throw + /// an invalid operation exceptions if they are used. + /// Continuous Reading Mode: Subscribe to data reception events, it runs a background thread, don't use Read methods + /// Manual Reading Mode: Data reception events are NEVER fired. No background threads are used. Use Read methods to receive data. + /// + /// + /// + /// The following code explains how to create a TCP server. + /// + /// using System.Text; + /// using Unosquare.Swan.Networking; + /// + /// class Example + /// { + /// static void Main() + /// { + /// // create a new connection listener on a specific port + /// var connectionListener = new ConnectionListener(1337); + /// + /// // handle the OnConnectionAccepting event + /// connectionListener.OnConnectionAccepted += (s, e) => + /// { + /// // create a new connection + /// using (var con = new Connection(e.Client)) + /// { + /// con.WriteLineAsync("Hello world!").Wait(); + /// } + /// }; + /// + /// connectionListener.Start(); + /// } + /// } + /// + /// The following code describes how to create a TCP client. + /// + /// using System.Net.Sockets; + /// using System.Text; + /// using System.Threading.Tasks; + /// using Unosquare.Swan.Networking; + /// + /// class Example + /// { + /// static async Task Main() + /// { + /// // create a new TcpClient object + /// var client = new TcpClient(); + /// + /// // connect to a specific address and port + /// client.Connect("localhost", 1337); + /// + /// //create a new connection with specific encoding, + /// //new line sequence and continuous reading disabled + /// using (var cn = new Connection(client, Encoding.UTF8, "\r\n", true, 0)) + /// { + /// var response = await cn.ReadTextAsync(); + /// } + /// } + /// } + /// + /// + public sealed class Connection : IDisposable { + // New Line definitions for reading. This applies to both, events and read methods + private readonly String _newLineSequence; + + private readonly Byte[] _newLineSequenceBytes; + private readonly Char[] _newLineSequenceChars; + private readonly String[] _newLineSequenceLineSplitter; + private readonly Byte[] _receiveBuffer; + private readonly TimeSpan _continuousReadingInterval = TimeSpan.FromMilliseconds(5); + private readonly Queue _readLineBuffer = new Queue(); + private readonly ManualResetEvent _writeDone = new ManualResetEvent(true); + + // Disconnect and Dispose + private Boolean _hasDisposed; + + private Int32 _disconnectCalls; + + // Continuous Reading + private Thread _continuousReadingThread; + + private Int32 _receiveBufferPointer; + + // Reading and writing + private Task _readTask; + /// - /// Represents a network connection either on the server or on the client. It wraps a TcpClient - /// and its corresponding network streams. It is capable of working in 2 modes. Typically on the server side - /// you will need to enable continuous reading and events. On the client side you may want to disable continuous reading - /// and use the Read methods available. In continuous reading mode Read methods are not available and will throw - /// an invalid operation exceptions if they are used. - /// Continuous Reading Mode: Subscribe to data reception events, it runs a background thread, don't use Read methods - /// Manual Reading Mode: Data reception events are NEVER fired. No background threads are used. Use Read methods to receive data. + /// Initializes a new instance of the class. /// - /// - /// - /// The following code explains how to create a TCP server. - /// - /// using System.Text; - /// using Unosquare.Swan.Networking; - /// - /// class Example - /// { - /// static void Main() - /// { - /// // create a new connection listener on a specific port - /// var connectionListener = new ConnectionListener(1337); - /// - /// // handle the OnConnectionAccepting event - /// connectionListener.OnConnectionAccepted += (s, e) => - /// { - /// // create a new connection - /// using (var con = new Connection(e.Client)) - /// { - /// con.WriteLineAsync("Hello world!").Wait(); - /// } - /// }; - /// - /// connectionListener.Start(); - /// } - /// } - /// - /// The following code describes how to create a TCP client. - /// - /// using System.Net.Sockets; - /// using System.Text; - /// using System.Threading.Tasks; - /// using Unosquare.Swan.Networking; - /// - /// class Example - /// { - /// static async Task Main() - /// { - /// // create a new TcpClient object - /// var client = new TcpClient(); - /// - /// // connect to a specific address and port - /// client.Connect("localhost", 1337); - /// - /// //create a new connection with specific encoding, - /// //new line sequence and continuous reading disabled - /// using (var cn = new Connection(client, Encoding.UTF8, "\r\n", true, 0)) - /// { - /// var response = await cn.ReadTextAsync(); - /// } - /// } - /// } - /// - /// - public sealed class Connection : IDisposable - { - // New Line definitions for reading. This applies to both, events and read methods - private readonly string _newLineSequence; - - private readonly byte[] _newLineSequenceBytes; - private readonly char[] _newLineSequenceChars; - private readonly string[] _newLineSequenceLineSplitter; - private readonly byte[] _receiveBuffer; - private readonly TimeSpan _continuousReadingInterval = TimeSpan.FromMilliseconds(5); - private readonly Queue _readLineBuffer = new Queue(); - private readonly ManualResetEvent _writeDone = new ManualResetEvent(true); - - // Disconnect and Dispose - private bool _hasDisposed; - - private int _disconnectCalls; - - // Continuous Reading - private Thread _continuousReadingThread; - - private int _receiveBufferPointer; - - // Reading and writing - private Task _readTask; - - /// - /// Initializes a new instance of the class. - /// - /// The client. - /// The text encoding. - /// The new line sequence used for read and write operations. - /// if set to true [disable continuous reading]. - /// Size of the block. -- set to 0 or less to disable. - public Connection( - TcpClient client, - Encoding textEncoding, - string newLineSequence, - bool disableContinuousReading, - int blockSize) - { - // Setup basic properties - Id = Guid.NewGuid(); - TextEncoding = textEncoding; - - // Setup new line sequence - if (string.IsNullOrEmpty(newLineSequence)) - throw new ArgumentException("Argument cannot be null", nameof(newLineSequence)); - - _newLineSequence = newLineSequence; - _newLineSequenceBytes = TextEncoding.GetBytes(_newLineSequence); - _newLineSequenceChars = _newLineSequence.ToCharArray(); - _newLineSequenceLineSplitter = new[] { _newLineSequence }; - - // Setup Connection timers - ConnectionStartTimeUtc = DateTime.UtcNow; - DataReceivedLastTimeUtc = ConnectionStartTimeUtc; - DataSentLastTimeUtc = ConnectionStartTimeUtc; - - // Setup connection properties - RemoteClient = client; - LocalEndPoint = client.Client.LocalEndPoint as IPEndPoint; - NetworkStream = RemoteClient.GetStream(); - RemoteEndPoint = RemoteClient.Client.RemoteEndPoint as IPEndPoint; - - // Setup buffers - _receiveBuffer = new byte[RemoteClient.ReceiveBufferSize * 2]; - ProtocolBlockSize = blockSize; - _receiveBufferPointer = 0; - - // Setup continuous reading mode if enabled - if (disableContinuousReading) return; - + /// The client. + /// The text encoding. + /// The new line sequence used for read and write operations. + /// if set to true [disable continuous reading]. + /// Size of the block. -- set to 0 or less to disable. + public Connection( + TcpClient client, + Encoding textEncoding, + String newLineSequence, + Boolean disableContinuousReading, + Int32 blockSize) { + // Setup basic properties + this.Id = Guid.NewGuid(); + this.TextEncoding = textEncoding; + + // Setup new line sequence + if(String.IsNullOrEmpty(newLineSequence)) { + throw new ArgumentException("Argument cannot be null", nameof(newLineSequence)); + } + + this._newLineSequence = newLineSequence; + this._newLineSequenceBytes = this.TextEncoding.GetBytes(this._newLineSequence); + this._newLineSequenceChars = this._newLineSequence.ToCharArray(); + this._newLineSequenceLineSplitter = new[] { this._newLineSequence }; + + // Setup Connection timers + this.ConnectionStartTimeUtc = DateTime.UtcNow; + this.DataReceivedLastTimeUtc = this.ConnectionStartTimeUtc; + this.DataSentLastTimeUtc = this.ConnectionStartTimeUtc; + + // Setup connection properties + this.RemoteClient = client; + this.LocalEndPoint = client.Client.LocalEndPoint as IPEndPoint; + this.NetworkStream = this.RemoteClient.GetStream(); + this.RemoteEndPoint = this.RemoteClient.Client.RemoteEndPoint as IPEndPoint; + + // Setup buffers + this._receiveBuffer = new Byte[this.RemoteClient.ReceiveBufferSize * 2]; + this.ProtocolBlockSize = blockSize; + this._receiveBufferPointer = 0; + + // Setup continuous reading mode if enabled + if(disableContinuousReading) { + return; + } + #if NETSTANDARD1_3 ThreadPool.QueueUserWorkItem(PerformContinuousReading, this); #else - ThreadPool.GetAvailableThreads(out var availableWorkerThreads, out _); - ThreadPool.GetMaxThreads(out var maxWorkerThreads, out var _); - - var activeThreadPoolTreads = maxWorkerThreads - availableWorkerThreads; - - if (activeThreadPoolTreads < Environment.ProcessorCount / 4) - { - ThreadPool.QueueUserWorkItem(PerformContinuousReading, this); - } - else - { - new Thread(PerformContinuousReading) { IsBackground = true }.Start(); - } + ThreadPool.GetAvailableThreads(out Int32 availableWorkerThreads, out _); + ThreadPool.GetMaxThreads(out Int32 maxWorkerThreads, out Int32 _); + + Int32 activeThreadPoolTreads = maxWorkerThreads - availableWorkerThreads; + + if(activeThreadPoolTreads < Environment.ProcessorCount / 4) { + ThreadPool.QueueUserWorkItem(this.PerformContinuousReading, this); + } else { + new Thread(this.PerformContinuousReading) { IsBackground = true }.Start(); + } #endif - } - - /// - /// Initializes a new instance of the class in continuous reading mode. - /// It uses UTF8 encoding, CRLF as a new line sequence and disables a protocol block size. - /// - /// The client. - public Connection(TcpClient client) - : this(client, Encoding.UTF8, "\r\n", false, 0) - { - // placeholder - } - - /// - /// Initializes a new instance of the class in continuous reading mode. - /// It uses UTF8 encoding, disables line sequences, and uses a protocol block size instead. - /// - /// The client. - /// Size of the block. - public Connection(TcpClient client, int blockSize) - : this(client, Encoding.UTF8, new string('\n', blockSize + 1), false, blockSize) - { - // placeholder - } - - #region Events - - /// - /// Occurs when the receive buffer has encounters a new line sequence, the buffer is flushed or the buffer is full. - /// - public event EventHandler DataReceived = (s, e) => { }; - - /// - /// Occurs when an error occurs while upgrading, sending, or receiving data in this client - /// - public event EventHandler ConnectionFailure = (s, e) => { }; - - /// - /// Occurs when a client is disconnected - /// - public event EventHandler ClientDisconnected = (s, e) => { }; - - #endregion - - #region Properties - - /// - /// Gets the unique identifier of this connection. - /// This field is filled out upon instantiation of this class. - /// - /// - /// The identifier. - /// - public Guid Id { get; } - - /// - /// Gets the active stream. Returns an SSL stream if the connection is secure, otherwise returns - /// the underlying NetworkStream. - /// - /// - /// The active stream. - /// - public Stream ActiveStream => SecureStream ?? NetworkStream as Stream; - - /// - /// Gets a value indicating whether the current connection stream is an SSL stream. - /// - /// - /// true if this instance is active stream secure; otherwise, false. - /// - public bool IsActiveStreamSecure => SecureStream != null; - - /// - /// Gets the text encoding for send and receive operations. - /// - /// - /// The text encoding. - /// - public Encoding TextEncoding { get; } - - /// - /// Gets the remote end point of this TCP connection. - /// - /// - /// The remote end point. - /// - public IPEndPoint RemoteEndPoint { get; } - - /// - /// Gets the local end point of this TCP connection. - /// - /// - /// The local end point. - /// - public IPEndPoint LocalEndPoint { get; } - - /// - /// Gets the remote client of this TCP connection. - /// - /// - /// The remote client. - /// - public TcpClient RemoteClient { get; private set; } - - /// - /// When in continuous reading mode, and if set to greater than 0, - /// a Data reception event will be fired whenever the amount of bytes - /// determined by this property has been received. Useful for fixed-length message protocols. - /// - /// - /// The size of the protocol block. - /// - public int ProtocolBlockSize { get; } - - /// - /// Gets a value indicating whether this connection is in continuous reading mode. - /// Remark: Whenever a disconnect event occurs, the background thread is terminated - /// and this property will return false whenever the reading thread is not active. - /// Therefore, even if continuous reading was not disabled in the constructor, this property - /// might return false. - /// - /// - /// true if this instance is continuous reading enabled; otherwise, false. - /// - public bool IsContinuousReadingEnabled => _continuousReadingThread != null; - - /// - /// Gets the start time at which the connection was started in UTC. - /// - /// - /// The connection start time UTC. - /// - public DateTime ConnectionStartTimeUtc { get; } - - /// - /// Gets the start time at which the connection was started in local time. - /// - /// - /// The connection start time. - /// - public DateTime ConnectionStartTime => ConnectionStartTimeUtc.ToLocalTime(); - - /// - /// Gets the duration of the connection. - /// - /// - /// The duration of the connection. - /// - public TimeSpan ConnectionDuration => DateTime.UtcNow.Subtract(ConnectionStartTimeUtc); - - /// - /// Gets the last time data was received at in UTC. - /// - /// - /// The data received last time UTC. - /// - public DateTime DataReceivedLastTimeUtc { get; private set; } - - /// - /// Gets how long has elapsed since data was last received. - /// - public TimeSpan DataReceivedIdleDuration => DateTime.UtcNow.Subtract(DataReceivedLastTimeUtc); - - /// - /// Gets the last time at which data was sent in UTC. - /// - /// - /// The data sent last time UTC. - /// - public DateTime DataSentLastTimeUtc { get; private set; } - - /// - /// Gets how long has elapsed since data was last sent. - /// - /// - /// The duration of the data sent idle. - /// - public TimeSpan DataSentIdleDuration => DateTime.UtcNow.Subtract(DataSentLastTimeUtc); - - /// - /// Gets a value indicating whether this connection is connected. - /// Remarks: This property polls the socket internally and checks if it is available to read data from it. - /// If disconnect has been called, then this property will return false. - /// - /// - /// true if this instance is connected; otherwise, false. - /// - public bool IsConnected - { - get - { - if (_disconnectCalls > 0) - return false; - - try - { - var socket = RemoteClient.Client; - var pollResult = !((socket.Poll(1000, SelectMode.SelectRead) - && (NetworkStream.DataAvailable == false)) || !socket.Connected); - - if (pollResult == false) - Disconnect(); - - return pollResult; - } - catch - { - Disconnect(); - return false; - } - } - } - - private NetworkStream NetworkStream { get; set; } - - private SslStream SecureStream { get; set; } - - #endregion - - #region Read Methods - - /// - /// Reads data from the remote client asynchronously and with the given timeout. - /// - /// The timeout. - /// The cancellation token. - /// A byte array containing the results of encoding the specified set of characters. - /// Read methods have been disabled because continuous reading is enabled. - /// Reading data from {ActiveStream} timed out in {timeout.TotalMilliseconds} m. - public async Task ReadDataAsync(TimeSpan timeout, CancellationToken ct = default) - { - if (IsContinuousReadingEnabled) - { - throw new InvalidOperationException( - "Read methods have been disabled because continuous reading is enabled."); - } - - if (RemoteClient == null) - { - throw new InvalidOperationException("An open connection is required"); - } - - var receiveBuffer = new byte[RemoteClient.ReceiveBufferSize * 2]; - var receiveBuilder = new List(receiveBuffer.Length); - - try - { - var startTime = DateTime.UtcNow; - - while (receiveBuilder.Count <= 0) - { - if (DateTime.UtcNow.Subtract(startTime) >= timeout) - { - throw new TimeoutException( - $"Reading data from {ActiveStream} timed out in {timeout.TotalMilliseconds} ms"); - } - - if (_readTask == null) - _readTask = ActiveStream.ReadAsync(receiveBuffer, 0, receiveBuffer.Length, ct); - - if (_readTask.Wait(_continuousReadingInterval)) - { - var bytesReceivedCount = _readTask.Result; - if (bytesReceivedCount > 0) - { - DataReceivedLastTimeUtc = DateTime.UtcNow; - var buffer = new byte[bytesReceivedCount]; - Array.Copy(receiveBuffer, 0, buffer, 0, bytesReceivedCount); - receiveBuilder.AddRange(buffer); - } - - _readTask = null; - } - else - { - await Task.Delay(_continuousReadingInterval, ct).ConfigureAwait(false); - } - } - } - catch (Exception ex) - { - ex.Error(typeof(Connection).FullName, "Error while reading network stream data asynchronously."); - throw; - } - - return receiveBuilder.ToArray(); - } - - /// - /// Reads data asynchronously from the remote stream with a 5000 millisecond timeout. - /// - /// The cancellation token. - /// A byte array containing the results the specified sequence of bytes. - public Task ReadDataAsync(CancellationToken ct = default) - => ReadDataAsync(TimeSpan.FromSeconds(5), ct); - - /// - /// Asynchronously reads data as text with the given timeout. - /// - /// The timeout. - /// The cancellation token. - /// A that contains the results of decoding the specified sequence of bytes. - public async Task ReadTextAsync(TimeSpan timeout, CancellationToken ct = default) - { - var buffer = await ReadDataAsync(timeout, ct).ConfigureAwait(false); - return buffer == null ? null : TextEncoding.GetString(buffer); - } - - /// - /// Asynchronously reads data as text with a 5000 millisecond timeout. - /// - /// The cancellation token. - /// When this method completes successfully, it returns the contents of the file as a text string. - public Task ReadTextAsync(CancellationToken ct = default) - => ReadTextAsync(TimeSpan.FromSeconds(5), ct); - - /// - /// Performs the same task as this method's overload but it defaults to a read timeout of 30 seconds. - /// - /// The cancellation token. - /// - /// A task that represents the asynchronous read operation. The value of the TResult parameter - /// contains the next line from the stream, or is null if all the characters have been read. - /// - public Task ReadLineAsync(CancellationToken ct = default) - => ReadLineAsync(TimeSpan.FromSeconds(30), ct); - - /// - /// Reads the next available line of text in queue. Return null when no text is read. - /// This method differs from the rest of the read methods because it keeps an internal - /// queue of lines that are read from the stream and only returns the one line next in the queue. - /// It is only recommended to use this method when you are working with text-based protocols - /// and the rest of the read methods are not called. - /// - /// The timeout. - /// The cancellation token. - /// A task with a string line from the queue. - /// Read methods have been disabled because continuous reading is enabled. - public async Task ReadLineAsync(TimeSpan timeout, CancellationToken ct = default) - { - if (IsContinuousReadingEnabled) - { - throw new InvalidOperationException( - "Read methods have been disabled because continuous reading is enabled."); - } - - if (_readLineBuffer.Count > 0) - return _readLineBuffer.Dequeue(); - - var builder = new StringBuilder(); - - while (true) - { - var text = await ReadTextAsync(timeout, ct).ConfigureAwait(false); - if (text.Length == 0) - break; - - builder.Append(text); - - if (text.EndsWith(_newLineSequence) == false) continue; - - var lines = builder.ToString().TrimEnd(_newLineSequenceChars) - .Split(_newLineSequenceLineSplitter, StringSplitOptions.None); - foreach (var item in lines) - _readLineBuffer.Enqueue(item); - - break; - } - - return _readLineBuffer.Count > 0 ? _readLineBuffer.Dequeue() : null; - } - - #endregion - - #region Write Methods - - /// - /// Writes data asynchronously. - /// - /// The buffer. - /// if set to true [force flush]. - /// The cancellation token. - /// A task that represents the asynchronous write operation. - public async Task WriteDataAsync(byte[] buffer, bool forceFlush, CancellationToken ct = default) - { - try - { - _writeDone.WaitOne(); - _writeDone.Reset(); - await ActiveStream.WriteAsync(buffer, 0, buffer.Length, ct).ConfigureAwait(false); - if (forceFlush) - await ActiveStream.FlushAsync(ct).ConfigureAwait(false); - - DataSentLastTimeUtc = DateTime.UtcNow; - } - finally - { - _writeDone.Set(); - } - } - - /// - /// Writes text asynchronously. - /// - /// The text. - /// The cancellation token. - /// A task that represents the asynchronous write operation. - public Task WriteTextAsync(string text, CancellationToken ct = default) - => WriteTextAsync(text, TextEncoding, ct); - - /// - /// Writes text asynchronously. - /// - /// The text. - /// The encoding. - /// The cancellation token. - /// A task that represents the asynchronous write operation. - public Task WriteTextAsync(string text, Encoding encoding, CancellationToken ct = default) - => WriteDataAsync(encoding.GetBytes(text), true, ct); - - /// - /// Writes a line of text asynchronously. - /// The new line sequence is added automatically at the end of the line. - /// - /// The line. - /// The encoding. - /// The cancellation token. - /// A task that represents the asynchronous write operation. - public Task WriteLineAsync(string line, Encoding encoding, CancellationToken ct = default) - => WriteDataAsync(encoding.GetBytes($"{line}{_newLineSequence}"), true, ct); - - /// - /// Writes a line of text asynchronously. - /// The new line sequence is added automatically at the end of the line. - /// - /// The line. - /// The cancellation token. - /// A task that represents the asynchronous write operation. - public Task WriteLineAsync(string line, CancellationToken ct = default) - => WriteLineAsync(line, TextEncoding, ct); - - #endregion - - #region Socket Methods - - /// - /// Upgrades the active stream to an SSL stream if this connection object is hosted in the server. - /// - /// The server certificate. - /// true if the object is hosted in the server; otherwise, false. - public async Task UpgradeToSecureAsServerAsync(X509Certificate2 serverCertificate) - { - if (IsActiveStreamSecure) - return true; - - _writeDone.WaitOne(); - - SslStream secureStream = null; - - try - { - secureStream = new SslStream(NetworkStream, true); - await secureStream.AuthenticateAsServerAsync(serverCertificate).ConfigureAwait(false); - SecureStream = secureStream; - return true; - } - catch (Exception ex) - { - ConnectionFailure(this, new ConnectionFailureEventArgs(ex)); - secureStream?.Dispose(); - - return false; - } - } - - /// - /// Upgrades the active stream to an SSL stream if this connection object is hosted in the client. - /// - /// The hostname. - /// The callback. - /// A tasks with true if the upgrade to SSL was successful; otherwise, false. - public async Task UpgradeToSecureAsClientAsync( - string hostname = null, - RemoteCertificateValidationCallback callback = null) - { - if (IsActiveStreamSecure) - return true; - - var secureStream = callback == null - ? new SslStream(NetworkStream, true) - : new SslStream(NetworkStream, true, callback); - - try - { - await secureStream.AuthenticateAsClientAsync(hostname ?? Network.HostName.ToLowerInvariant()).ConfigureAwait(false); - SecureStream = secureStream; - } - catch (Exception ex) - { - secureStream.Dispose(); - ConnectionFailure(this, new ConnectionFailureEventArgs(ex)); - return false; - } - - return true; - } - - /// - /// Disconnects this connection. - /// - public void Disconnect() - { - if (_disconnectCalls > 0) - return; - - _disconnectCalls++; - _writeDone.WaitOne(); - - try - { - ClientDisconnected(this, EventArgs.Empty); - } - catch - { - // ignore - } - - try - { + } + + /// + /// Initializes a new instance of the class in continuous reading mode. + /// It uses UTF8 encoding, CRLF as a new line sequence and disables a protocol block size. + /// + /// The client. + public Connection(TcpClient client) + : this(client, Encoding.UTF8, "\r\n", false, 0) { + // placeholder + } + + /// + /// Initializes a new instance of the class in continuous reading mode. + /// It uses UTF8 encoding, disables line sequences, and uses a protocol block size instead. + /// + /// The client. + /// Size of the block. + public Connection(TcpClient client, Int32 blockSize) + : this(client, Encoding.UTF8, new String('\n', blockSize + 1), false, blockSize) { + // placeholder + } + + #region Events + + /// + /// Occurs when the receive buffer has encounters a new line sequence, the buffer is flushed or the buffer is full. + /// + public event EventHandler DataReceived = (s, e) => { }; + + /// + /// Occurs when an error occurs while upgrading, sending, or receiving data in this client + /// + public event EventHandler ConnectionFailure = (s, e) => { }; + + /// + /// Occurs when a client is disconnected + /// + public event EventHandler ClientDisconnected = (s, e) => { }; + + #endregion + + #region Properties + + /// + /// Gets the unique identifier of this connection. + /// This field is filled out upon instantiation of this class. + /// + /// + /// The identifier. + /// + public Guid Id { + get; + } + + /// + /// Gets the active stream. Returns an SSL stream if the connection is secure, otherwise returns + /// the underlying NetworkStream. + /// + /// + /// The active stream. + /// + public Stream ActiveStream => this.SecureStream ?? this.NetworkStream as Stream; + + /// + /// Gets a value indicating whether the current connection stream is an SSL stream. + /// + /// + /// true if this instance is active stream secure; otherwise, false. + /// + public Boolean IsActiveStreamSecure => this.SecureStream != null; + + /// + /// Gets the text encoding for send and receive operations. + /// + /// + /// The text encoding. + /// + public Encoding TextEncoding { + get; + } + + /// + /// Gets the remote end point of this TCP connection. + /// + /// + /// The remote end point. + /// + public IPEndPoint RemoteEndPoint { + get; + } + + /// + /// Gets the local end point of this TCP connection. + /// + /// + /// The local end point. + /// + public IPEndPoint LocalEndPoint { + get; + } + + /// + /// Gets the remote client of this TCP connection. + /// + /// + /// The remote client. + /// + public TcpClient RemoteClient { + get; private set; + } + + /// + /// When in continuous reading mode, and if set to greater than 0, + /// a Data reception event will be fired whenever the amount of bytes + /// determined by this property has been received. Useful for fixed-length message protocols. + /// + /// + /// The size of the protocol block. + /// + public Int32 ProtocolBlockSize { + get; + } + + /// + /// Gets a value indicating whether this connection is in continuous reading mode. + /// Remark: Whenever a disconnect event occurs, the background thread is terminated + /// and this property will return false whenever the reading thread is not active. + /// Therefore, even if continuous reading was not disabled in the constructor, this property + /// might return false. + /// + /// + /// true if this instance is continuous reading enabled; otherwise, false. + /// + public Boolean IsContinuousReadingEnabled => this._continuousReadingThread != null; + + /// + /// Gets the start time at which the connection was started in UTC. + /// + /// + /// The connection start time UTC. + /// + public DateTime ConnectionStartTimeUtc { + get; + } + + /// + /// Gets the start time at which the connection was started in local time. + /// + /// + /// The connection start time. + /// + public DateTime ConnectionStartTime => this.ConnectionStartTimeUtc.ToLocalTime(); + + /// + /// Gets the duration of the connection. + /// + /// + /// The duration of the connection. + /// + public TimeSpan ConnectionDuration => DateTime.UtcNow.Subtract(this.ConnectionStartTimeUtc); + + /// + /// Gets the last time data was received at in UTC. + /// + /// + /// The data received last time UTC. + /// + public DateTime DataReceivedLastTimeUtc { + get; private set; + } + + /// + /// Gets how long has elapsed since data was last received. + /// + public TimeSpan DataReceivedIdleDuration => DateTime.UtcNow.Subtract(this.DataReceivedLastTimeUtc); + + /// + /// Gets the last time at which data was sent in UTC. + /// + /// + /// The data sent last time UTC. + /// + public DateTime DataSentLastTimeUtc { + get; private set; + } + + /// + /// Gets how long has elapsed since data was last sent. + /// + /// + /// The duration of the data sent idle. + /// + public TimeSpan DataSentIdleDuration => DateTime.UtcNow.Subtract(this.DataSentLastTimeUtc); + + /// + /// Gets a value indicating whether this connection is connected. + /// Remarks: This property polls the socket internally and checks if it is available to read data from it. + /// If disconnect has been called, then this property will return false. + /// + /// + /// true if this instance is connected; otherwise, false. + /// + public Boolean IsConnected { + get { + if(this._disconnectCalls > 0) { + return false; + } + + try { + Socket socket = this.RemoteClient.Client; + Boolean pollResult = !(socket.Poll(1000, SelectMode.SelectRead) + && this.NetworkStream.DataAvailable == false || !socket.Connected); + + if(pollResult == false) { + this.Disconnect(); + } + + return pollResult; + } catch { + this.Disconnect(); + return false; + } + } + } + + private NetworkStream NetworkStream { + get; set; + } + + private SslStream SecureStream { + get; set; + } + + #endregion + + #region Read Methods + + /// + /// Reads data from the remote client asynchronously and with the given timeout. + /// + /// The timeout. + /// The cancellation token. + /// A byte array containing the results of encoding the specified set of characters. + /// Read methods have been disabled because continuous reading is enabled. + /// Reading data from {ActiveStream} timed out in {timeout.TotalMilliseconds} m. + public async Task ReadDataAsync(TimeSpan timeout, CancellationToken ct = default) { + if(this.IsContinuousReadingEnabled) { + throw new InvalidOperationException( + "Read methods have been disabled because continuous reading is enabled."); + } + + if(this.RemoteClient == null) { + throw new InvalidOperationException("An open connection is required"); + } + + Byte[] receiveBuffer = new Byte[this.RemoteClient.ReceiveBufferSize * 2]; + List receiveBuilder = new List(receiveBuffer.Length); + + try { + DateTime startTime = DateTime.UtcNow; + + while(receiveBuilder.Count <= 0) { + if(DateTime.UtcNow.Subtract(startTime) >= timeout) { + throw new TimeoutException( + $"Reading data from {this.ActiveStream} timed out in {timeout.TotalMilliseconds} ms"); + } + + if(this._readTask == null) { + this._readTask = this.ActiveStream.ReadAsync(receiveBuffer, 0, receiveBuffer.Length, ct); + } + + if(this._readTask.Wait(this._continuousReadingInterval)) { + Int32 bytesReceivedCount = this._readTask.Result; + if(bytesReceivedCount > 0) { + this.DataReceivedLastTimeUtc = DateTime.UtcNow; + Byte[] buffer = new Byte[bytesReceivedCount]; + Array.Copy(receiveBuffer, 0, buffer, 0, bytesReceivedCount); + receiveBuilder.AddRange(buffer); + } + + this._readTask = null; + } else { + await Task.Delay(this._continuousReadingInterval, ct).ConfigureAwait(false); + } + } + } catch(Exception ex) { + ex.Error(typeof(Connection).FullName, "Error while reading network stream data asynchronously."); + throw; + } + + return receiveBuilder.ToArray(); + } + + /// + /// Reads data asynchronously from the remote stream with a 5000 millisecond timeout. + /// + /// The cancellation token. + /// A byte array containing the results the specified sequence of bytes. + public Task ReadDataAsync(CancellationToken ct = default) + => this.ReadDataAsync(TimeSpan.FromSeconds(5), ct); + + /// + /// Asynchronously reads data as text with the given timeout. + /// + /// The timeout. + /// The cancellation token. + /// A that contains the results of decoding the specified sequence of bytes. + public async Task ReadTextAsync(TimeSpan timeout, CancellationToken ct = default) { + Byte[] buffer = await this.ReadDataAsync(timeout, ct).ConfigureAwait(false); + return buffer == null ? null : this.TextEncoding.GetString(buffer); + } + + /// + /// Asynchronously reads data as text with a 5000 millisecond timeout. + /// + /// The cancellation token. + /// When this method completes successfully, it returns the contents of the file as a text string. + public Task ReadTextAsync(CancellationToken ct = default) + => this.ReadTextAsync(TimeSpan.FromSeconds(5), ct); + + /// + /// Performs the same task as this method's overload but it defaults to a read timeout of 30 seconds. + /// + /// The cancellation token. + /// + /// A task that represents the asynchronous read operation. The value of the TResult parameter + /// contains the next line from the stream, or is null if all the characters have been read. + /// + public Task ReadLineAsync(CancellationToken ct = default) + => this.ReadLineAsync(TimeSpan.FromSeconds(30), ct); + + /// + /// Reads the next available line of text in queue. Return null when no text is read. + /// This method differs from the rest of the read methods because it keeps an internal + /// queue of lines that are read from the stream and only returns the one line next in the queue. + /// It is only recommended to use this method when you are working with text-based protocols + /// and the rest of the read methods are not called. + /// + /// The timeout. + /// The cancellation token. + /// A task with a string line from the queue. + /// Read methods have been disabled because continuous reading is enabled. + public async Task ReadLineAsync(TimeSpan timeout, CancellationToken ct = default) { + if(this.IsContinuousReadingEnabled) { + throw new InvalidOperationException( + "Read methods have been disabled because continuous reading is enabled."); + } + + if(this._readLineBuffer.Count > 0) { + return this._readLineBuffer.Dequeue(); + } + + StringBuilder builder = new StringBuilder(); + + while(true) { + String text = await this.ReadTextAsync(timeout, ct).ConfigureAwait(false); + if(text.Length == 0) { + break; + } + + _ = builder.Append(text); + + if(text.EndsWith(this._newLineSequence) == false) { + continue; + } + + String[] lines = builder.ToString().TrimEnd(this._newLineSequenceChars) + .Split(this._newLineSequenceLineSplitter, StringSplitOptions.None); + foreach(String item in lines) { + this._readLineBuffer.Enqueue(item); + } + + break; + } + + return this._readLineBuffer.Count > 0 ? this._readLineBuffer.Dequeue() : null; + } + + #endregion + + #region Write Methods + + /// + /// Writes data asynchronously. + /// + /// The buffer. + /// if set to true [force flush]. + /// The cancellation token. + /// A task that represents the asynchronous write operation. + public async Task WriteDataAsync(Byte[] buffer, Boolean forceFlush, CancellationToken ct = default) { + try { + _ = this._writeDone.WaitOne(); + _ = this._writeDone.Reset(); + await this.ActiveStream.WriteAsync(buffer, 0, buffer.Length, ct).ConfigureAwait(false); + if(forceFlush) { + await this.ActiveStream.FlushAsync(ct).ConfigureAwait(false); + } + + this.DataSentLastTimeUtc = DateTime.UtcNow; + } finally { + _ = this._writeDone.Set(); + } + } + + /// + /// Writes text asynchronously. + /// + /// The text. + /// The cancellation token. + /// A task that represents the asynchronous write operation. + public Task WriteTextAsync(String text, CancellationToken ct = default) + => this.WriteTextAsync(text, this.TextEncoding, ct); + + /// + /// Writes text asynchronously. + /// + /// The text. + /// The encoding. + /// The cancellation token. + /// A task that represents the asynchronous write operation. + public Task WriteTextAsync(String text, Encoding encoding, CancellationToken ct = default) + => this.WriteDataAsync(encoding.GetBytes(text), true, ct); + + /// + /// Writes a line of text asynchronously. + /// The new line sequence is added automatically at the end of the line. + /// + /// The line. + /// The encoding. + /// The cancellation token. + /// A task that represents the asynchronous write operation. + public Task WriteLineAsync(String line, Encoding encoding, CancellationToken ct = default) + => this.WriteDataAsync(encoding.GetBytes($"{line}{this._newLineSequence}"), true, ct); + + /// + /// Writes a line of text asynchronously. + /// The new line sequence is added automatically at the end of the line. + /// + /// The line. + /// The cancellation token. + /// A task that represents the asynchronous write operation. + public Task WriteLineAsync(String line, CancellationToken ct = default) + => this.WriteLineAsync(line, this.TextEncoding, ct); + + #endregion + + #region Socket Methods + + /// + /// Upgrades the active stream to an SSL stream if this connection object is hosted in the server. + /// + /// The server certificate. + /// true if the object is hosted in the server; otherwise, false. + public async Task UpgradeToSecureAsServerAsync(X509Certificate2 serverCertificate) { + if(this.IsActiveStreamSecure) { + return true; + } + + _ = this._writeDone.WaitOne(); + + SslStream secureStream = null; + + try { + secureStream = new SslStream(this.NetworkStream, true); + await secureStream.AuthenticateAsServerAsync(serverCertificate).ConfigureAwait(false); + this.SecureStream = secureStream; + return true; + } catch(Exception ex) { + ConnectionFailure(this, new ConnectionFailureEventArgs(ex)); + secureStream?.Dispose(); + + return false; + } + } + + /// + /// Upgrades the active stream to an SSL stream if this connection object is hosted in the client. + /// + /// The hostname. + /// The callback. + /// A tasks with true if the upgrade to SSL was successful; otherwise, false. + public async Task UpgradeToSecureAsClientAsync( + String hostname = null, + RemoteCertificateValidationCallback callback = null) { + if(this.IsActiveStreamSecure) { + return true; + } + + SslStream secureStream = callback == null + ? new SslStream(this.NetworkStream, true) + : new SslStream(this.NetworkStream, true, callback); + + try { + await secureStream.AuthenticateAsClientAsync(hostname ?? Network.HostName.ToLowerInvariant()).ConfigureAwait(false); + this.SecureStream = secureStream; + } catch(Exception ex) { + secureStream.Dispose(); + ConnectionFailure(this, new ConnectionFailureEventArgs(ex)); + return false; + } + + return true; + } + + /// + /// Disconnects this connection. + /// + public void Disconnect() { + if(this._disconnectCalls > 0) { + return; + } + + this._disconnectCalls++; + this._writeDone.WaitOne(); + + try { + ClientDisconnected(this, EventArgs.Empty); + } catch { + // ignore + } + + try { #if !NET452 RemoteClient.Dispose(); SecureStream?.Dispose(); NetworkStream?.Dispose(); #else - RemoteClient.Close(); - SecureStream?.Close(); - NetworkStream?.Close(); + this.RemoteClient.Close(); + this.SecureStream?.Close(); + this.NetworkStream?.Close(); #endif - } - catch - { - // ignored - } - finally - { - NetworkStream = null; - SecureStream = null; - RemoteClient = null; - _continuousReadingThread = null; - } - } - - #endregion - - #region Dispose - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public void Dispose() - { - if (_hasDisposed) - return; - - // Release managed resources - Disconnect(); - _continuousReadingThread = null; - _writeDone.Dispose(); - - _hasDisposed = true; - } - - #endregion - - #region Continuous Read Methods - - /// - /// Raises the receive buffer events. - /// - /// The received data. - /// Split function failed! This is terribly wrong. - private void RaiseReceiveBufferEvents(byte[] receivedData) - { - var moreAvailable = RemoteClient.Available > 0; - - foreach (var data in receivedData) - { - ProcessReceivedBlock(data, moreAvailable); - } - - // Check if we are left with some more stuff to handle - if (_receiveBufferPointer <= 0) - return; - - // Extract the segments split by newline terminated bytes - var sequences = _receiveBuffer.Skip(0).Take(_receiveBufferPointer).ToArray() - .Split(0, _newLineSequenceBytes); - - // Something really wrong happened - if (sequences.Count == 0) - throw new InvalidOperationException("Split function failed! This is terribly wrong!"); - - // We only have one sequence and it is not newline-terminated - // we don't have to do anything. - if (sequences.Count == 1 && sequences[0].EndsWith(_newLineSequenceBytes) == false) - return; - - // Process the events for each sequence - for (var i = 0; i < sequences.Count; i++) - { - var sequenceBytes = sequences[i]; - var isNewLineTerminated = sequences[i].EndsWith(_newLineSequenceBytes); - var isLast = i == sequences.Count - 1; - - if (isNewLineTerminated) - { - var eventArgs = new ConnectionDataReceivedEventArgs( - sequenceBytes, - ConnectionDataReceivedTrigger.NewLineSequenceEncountered, - isLast == false); - DataReceived(this, eventArgs); - } - - // Depending on the last segment determine what to do with the receive buffer - if (!isLast) continue; - - if (isNewLineTerminated) - { - // Simply reset the buffer pointer if the last segment was also terminated - _receiveBufferPointer = 0; - } - else - { - // If we have not received the termination sequence, then just shift the receive buffer to the left - // and adjust the pointer - Array.Copy(sequenceBytes, _receiveBuffer, sequenceBytes.Length); - _receiveBufferPointer = sequenceBytes.Length; - } - } - } - - private void ProcessReceivedBlock(byte data, bool moreAvailable) - { - _receiveBuffer[_receiveBufferPointer] = data; - _receiveBufferPointer++; - - // Block size reached - if (ProtocolBlockSize > 0 && _receiveBufferPointer >= ProtocolBlockSize) - { - SendBuffer(moreAvailable, ConnectionDataReceivedTrigger.BlockSizeReached); - return; - } - - // The receive buffer is full. Time to flush - if (_receiveBufferPointer >= _receiveBuffer.Length) - { - SendBuffer(moreAvailable, ConnectionDataReceivedTrigger.BufferFull); - } - } - - private void SendBuffer(bool moreAvailable, ConnectionDataReceivedTrigger trigger) - { - var eventBuffer = new byte[_receiveBuffer.Length]; - Array.Copy(_receiveBuffer, eventBuffer, eventBuffer.Length); - - DataReceived(this, - new ConnectionDataReceivedEventArgs( - eventBuffer, - trigger, - moreAvailable)); - _receiveBufferPointer = 0; - } - - private void PerformContinuousReading(object threadContext) - { - _continuousReadingThread = Thread.CurrentThread; - - // Check if the RemoteClient is still there - if (RemoteClient == null) return; - - var receiveBuffer = new byte[RemoteClient.ReceiveBufferSize * 2]; - - while (IsConnected && _disconnectCalls <= 0) - { - var doThreadSleep = false; - - try - { - if (_readTask == null) - _readTask = ActiveStream.ReadAsync(receiveBuffer, 0, receiveBuffer.Length); - - if (_readTask.Wait(_continuousReadingInterval)) - { - var bytesReceivedCount = _readTask.Result; - if (bytesReceivedCount > 0) - { - DataReceivedLastTimeUtc = DateTime.UtcNow; - var buffer = new byte[bytesReceivedCount]; - Array.Copy(receiveBuffer, 0, buffer, 0, bytesReceivedCount); - RaiseReceiveBufferEvents(buffer); - } - - _readTask = null; - } - else - { - doThreadSleep = _disconnectCalls <= 0; - } - } - catch (Exception ex) - { - ex.Log(nameof(Connection), "Continuous Read operation errored"); - } - finally - { - if (doThreadSleep) - Thread.Sleep(_continuousReadingInterval); - } - } - } - - #endregion - } + } catch { + // ignored + } finally { + this.NetworkStream = null; + this.SecureStream = null; + this.RemoteClient = null; + this._continuousReadingThread = null; + } + } + + #endregion + + #region Dispose + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() { + if(this._hasDisposed) { + return; + } + + // Release managed resources + this.Disconnect(); + this._continuousReadingThread = null; + this._writeDone.Dispose(); + + this._hasDisposed = true; + } + + #endregion + + #region Continuous Read Methods + + /// + /// Raises the receive buffer events. + /// + /// The received data. + /// Split function failed! This is terribly wrong. + private void RaiseReceiveBufferEvents(Byte[] receivedData) { + Boolean moreAvailable = this.RemoteClient.Available > 0; + + foreach(Byte data in receivedData) { + this.ProcessReceivedBlock(data, moreAvailable); + } + + // Check if we are left with some more stuff to handle + if(this._receiveBufferPointer <= 0) { + return; + } + + // Extract the segments split by newline terminated bytes + List sequences = this._receiveBuffer.Skip(0).Take(this._receiveBufferPointer).ToArray() + .Split(0, this._newLineSequenceBytes); + + // Something really wrong happened + if(sequences.Count == 0) { + throw new InvalidOperationException("Split function failed! This is terribly wrong!"); + } + + // We only have one sequence and it is not newline-terminated + // we don't have to do anything. + if(sequences.Count == 1 && sequences[0].EndsWith(this._newLineSequenceBytes) == false) { + return; + } + + // Process the events for each sequence + for(Int32 i = 0; i < sequences.Count; i++) { + Byte[] sequenceBytes = sequences[i]; + Boolean isNewLineTerminated = sequences[i].EndsWith(this._newLineSequenceBytes); + Boolean isLast = i == sequences.Count - 1; + + if(isNewLineTerminated) { + ConnectionDataReceivedEventArgs eventArgs = new ConnectionDataReceivedEventArgs( + sequenceBytes, + ConnectionDataReceivedTrigger.NewLineSequenceEncountered, + isLast == false); + DataReceived(this, eventArgs); + } + + // Depending on the last segment determine what to do with the receive buffer + if(!isLast) { + continue; + } + + if(isNewLineTerminated) { + // Simply reset the buffer pointer if the last segment was also terminated + this._receiveBufferPointer = 0; + } else { + // If we have not received the termination sequence, then just shift the receive buffer to the left + // and adjust the pointer + Array.Copy(sequenceBytes, this._receiveBuffer, sequenceBytes.Length); + this._receiveBufferPointer = sequenceBytes.Length; + } + } + } + + private void ProcessReceivedBlock(Byte data, Boolean moreAvailable) { + this._receiveBuffer[this._receiveBufferPointer] = data; + this._receiveBufferPointer++; + + // Block size reached + if(this.ProtocolBlockSize > 0 && this._receiveBufferPointer >= this.ProtocolBlockSize) { + this.SendBuffer(moreAvailable, ConnectionDataReceivedTrigger.BlockSizeReached); + return; + } + + // The receive buffer is full. Time to flush + if(this._receiveBufferPointer >= this._receiveBuffer.Length) { + this.SendBuffer(moreAvailable, ConnectionDataReceivedTrigger.BufferFull); + } + } + + private void SendBuffer(Boolean moreAvailable, ConnectionDataReceivedTrigger trigger) { + Byte[] eventBuffer = new Byte[this._receiveBuffer.Length]; + Array.Copy(this._receiveBuffer, eventBuffer, eventBuffer.Length); + + DataReceived(this, + new ConnectionDataReceivedEventArgs( + eventBuffer, + trigger, + moreAvailable)); + this._receiveBufferPointer = 0; + } + + private void PerformContinuousReading(Object threadContext) { + this._continuousReadingThread = Thread.CurrentThread; + + // Check if the RemoteClient is still there + if(this.RemoteClient == null) { + return; + } + + Byte[] receiveBuffer = new Byte[this.RemoteClient.ReceiveBufferSize * 2]; + + while(this.IsConnected && this._disconnectCalls <= 0) { + Boolean doThreadSleep = false; + + try { + if(this._readTask == null) { + this._readTask = this.ActiveStream.ReadAsync(receiveBuffer, 0, receiveBuffer.Length); + } + + if(this._readTask.Wait(this._continuousReadingInterval)) { + Int32 bytesReceivedCount = this._readTask.Result; + if(bytesReceivedCount > 0) { + this.DataReceivedLastTimeUtc = DateTime.UtcNow; + Byte[] buffer = new Byte[bytesReceivedCount]; + Array.Copy(receiveBuffer, 0, buffer, 0, bytesReceivedCount); + this.RaiseReceiveBufferEvents(buffer); + } + + this._readTask = null; + } else { + doThreadSleep = this._disconnectCalls <= 0; + } + } catch(Exception ex) { + ex.Log(nameof(Connection), "Continuous Read operation errored"); + } finally { + if(doThreadSleep) { + Thread.Sleep(this._continuousReadingInterval); + } + } + } + } + + #endregion + } } diff --git a/Unosquare.Swan/Networking/ConnectionListener.cs b/Unosquare.Swan/Networking/ConnectionListener.cs index 15dc5f4..ef9b4a4 100644 --- a/Unosquare.Swan/Networking/ConnectionListener.cs +++ b/Unosquare.Swan/Networking/ConnectionListener.cs @@ -1,258 +1,235 @@ -namespace Unosquare.Swan.Networking -{ - using Swan; - using System; - using System.Net; - using System.Net.Sockets; - using System.Threading; - using System.Threading.Tasks; - +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace Unosquare.Swan.Networking { + /// + /// TCP Listener manager with built-in events and asynchronous functionality. + /// This networking component is typically used when writing server software. + /// + /// + public sealed class ConnectionListener : IDisposable { + #region Private Declarations + + private readonly Object _stateLock = new Object(); + private TcpListener _listenerSocket; + private Boolean _cancellationPending; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0069:Verwerfbare Felder verwerfen", Justification = "")] + private CancellationTokenSource _cancelListening; + private Task _backgroundWorkerTask; + private Boolean _hasDisposed; + + #endregion + + #region Events + /// - /// TCP Listener manager with built-in events and asynchronous functionality. - /// This networking component is typically used when writing server software. + /// Occurs when a new connection requests a socket from the listener. + /// Set Cancel = true to prevent the TCP client from being accepted. /// - /// - public sealed class ConnectionListener : IDisposable - { - #region Private Declarations - - private readonly object _stateLock = new object(); - private TcpListener _listenerSocket; - private bool _cancellationPending; - private CancellationTokenSource _cancelListening; - private Task _backgroundWorkerTask; - private bool _hasDisposed; - - #endregion - - #region Events - - /// - /// Occurs when a new connection requests a socket from the listener. - /// Set Cancel = true to prevent the TCP client from being accepted. - /// - public event EventHandler OnConnectionAccepting = (s, e) => { }; - - /// - /// Occurs when a new connection is accepted. - /// - public event EventHandler OnConnectionAccepted = (s, e) => { }; - - /// - /// Occurs when a connection fails to get accepted - /// - public event EventHandler OnConnectionFailure = (s, e) => { }; - - /// - /// Occurs when the listener stops. - /// - public event EventHandler OnListenerStopped = (s, e) => { }; - - #endregion - - #region Constructors - - /// - /// Initializes a new instance of the class. - /// - /// The listen end point. - public ConnectionListener(IPEndPoint listenEndPoint) - { - Id = Guid.NewGuid(); - LocalEndPoint = listenEndPoint ?? throw new ArgumentNullException(nameof(listenEndPoint)); - } - - /// - /// Initializes a new instance of the class. - /// It uses the loopback address for listening. - /// - /// The listen port. - public ConnectionListener(int listenPort) - : this(new IPEndPoint(IPAddress.Loopback, listenPort)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The listen address. - /// The listen port. - public ConnectionListener(IPAddress listenAddress, int listenPort) - : this(new IPEndPoint(listenAddress, listenPort)) - { - } - - /// - /// Finalizes an instance of the class. - /// - ~ConnectionListener() - { - Dispose(false); - } - - #endregion - - #region Public Properties - - /// - /// Gets the local end point on which we are listening. - /// - /// - /// The local end point. - /// - public IPEndPoint LocalEndPoint { get; } - - /// - /// Gets a value indicating whether this listener is active. - /// - /// - /// true if this instance is listening; otherwise, false. - /// - public bool IsListening => _backgroundWorkerTask != null; - - /// - /// Gets a unique identifier that gets automatically assigned upon instantiation of this class. - /// - /// - /// The unique identifier. - /// - public Guid Id { get; } - - #endregion - - #region Start and Stop - - /// - /// Starts the listener in an asynchronous, non-blocking fashion. - /// Subscribe to the events of this class to gain access to connected client sockets. - /// - /// Cancellation has already been requested. This listener is not reusable. - public void Start() - { - lock (_stateLock) - { - if (_backgroundWorkerTask != null) - { - return; - } - - if (_cancellationPending) - { - throw new InvalidOperationException( - "Cancellation has already been requested. This listener is not reusable."); - } - - _backgroundWorkerTask = DoWorkAsync(); - } - } - - /// - /// Stops the listener from receiving new connections. - /// This does not prevent the listener from . - /// - public void Stop() - { - lock (_stateLock) - { - _cancellationPending = true; - _listenerSocket?.Stop(); - _cancelListening?.Cancel(); - _backgroundWorkerTask?.Wait(); - _backgroundWorkerTask = null; - _cancellationPending = false; - } - } - - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public override string ToString() => LocalEndPoint.ToString(); - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - private void Dispose(bool disposing) - { - if (_hasDisposed) - return; - - if (disposing) - { - // Release managed resources - Stop(); - } - - _hasDisposed = true; - } - - /// - /// Continuously checks for client connections until the Close method has been called. - /// - /// A task that represents the asynchronous connection operation. - private async Task DoWorkAsync() - { - _cancellationPending = false; - _listenerSocket = new TcpListener(LocalEndPoint); - _listenerSocket.Start(); - _cancelListening = new CancellationTokenSource(); - - try - { - while (_cancellationPending == false) - { - try - { - var client = await Task.Run(() => _listenerSocket.AcceptTcpClientAsync(), _cancelListening.Token).ConfigureAwait(false); - var acceptingArgs = new ConnectionAcceptingEventArgs(client); - OnConnectionAccepting(this, acceptingArgs); - - if (acceptingArgs.Cancel) - { + public event EventHandler OnConnectionAccepting = (s, e) => { }; + + /// + /// Occurs when a new connection is accepted. + /// + public event EventHandler OnConnectionAccepted = (s, e) => { }; + + /// + /// Occurs when a connection fails to get accepted + /// + public event EventHandler OnConnectionFailure = (s, e) => { }; + + /// + /// Occurs when the listener stops. + /// + public event EventHandler OnListenerStopped = (s, e) => { }; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The listen end point. + public ConnectionListener(IPEndPoint listenEndPoint) { + this.Id = Guid.NewGuid(); + this.LocalEndPoint = listenEndPoint ?? throw new ArgumentNullException(nameof(listenEndPoint)); + } + + /// + /// Initializes a new instance of the class. + /// It uses the loopback address for listening. + /// + /// The listen port. + public ConnectionListener(Int32 listenPort) + : this(new IPEndPoint(IPAddress.Loopback, listenPort)) { + } + + /// + /// Initializes a new instance of the class. + /// + /// The listen address. + /// The listen port. + public ConnectionListener(IPAddress listenAddress, Int32 listenPort) + : this(new IPEndPoint(listenAddress, listenPort)) { + } + + /// + /// Finalizes an instance of the class. + /// + ~ConnectionListener() { + this.Dispose(false); + } + + #endregion + + #region Public Properties + + /// + /// Gets the local end point on which we are listening. + /// + /// + /// The local end point. + /// + public IPEndPoint LocalEndPoint { + get; + } + + /// + /// Gets a value indicating whether this listener is active. + /// + /// + /// true if this instance is listening; otherwise, false. + /// + public Boolean IsListening => this._backgroundWorkerTask != null; + + /// + /// Gets a unique identifier that gets automatically assigned upon instantiation of this class. + /// + /// + /// The unique identifier. + /// + public Guid Id { + get; + } + + #endregion + + #region Start and Stop + + /// + /// Starts the listener in an asynchronous, non-blocking fashion. + /// Subscribe to the events of this class to gain access to connected client sockets. + /// + /// Cancellation has already been requested. This listener is not reusable. + public void Start() { + lock(this._stateLock) { + if(this._backgroundWorkerTask != null) { + return; + } + + if(this._cancellationPending) { + throw new InvalidOperationException( + "Cancellation has already been requested. This listener is not reusable."); + } + + this._backgroundWorkerTask = this.DoWorkAsync(); + } + } + + /// + /// Stops the listener from receiving new connections. + /// This does not prevent the listener from . + /// + public void Stop() { + lock(this._stateLock) { + this._cancellationPending = true; + this._listenerSocket?.Stop(); + this._cancelListening?.Cancel(); + this._backgroundWorkerTask?.Wait(); + this._backgroundWorkerTask = null; + this._cancellationPending = false; + } + } + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override String ToString() => this.LocalEndPoint.ToString(); + + /// + public void Dispose() { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + private void Dispose(Boolean disposing) { + if(this._hasDisposed) { + return; + } + + if(disposing) { + // Release managed resources + this.Stop(); + } + + this._hasDisposed = true; + } + + /// + /// Continuously checks for client connections until the Close method has been called. + /// + /// A task that represents the asynchronous connection operation. + private async Task DoWorkAsync() { + this._cancellationPending = false; + this._listenerSocket = new TcpListener(this.LocalEndPoint); + this._listenerSocket.Start(); + this._cancelListening = new CancellationTokenSource(); + + try { + while(this._cancellationPending == false) { + try { + TcpClient client = await Task.Run(() => this._listenerSocket.AcceptTcpClientAsync(), this._cancelListening.Token).ConfigureAwait(false); + ConnectionAcceptingEventArgs acceptingArgs = new ConnectionAcceptingEventArgs(client); + OnConnectionAccepting(this, acceptingArgs); + + if(acceptingArgs.Cancel) { #if !NET452 client.Dispose(); #else - client.Close(); + client.Close(); #endif - continue; - } - - OnConnectionAccepted(this, new ConnectionAcceptedEventArgs(client)); - } - catch (Exception ex) - { - OnConnectionFailure(this, new ConnectionFailureEventArgs(ex)); - } - } - - OnListenerStopped(this, new ConnectionListenerStoppedEventArgs(LocalEndPoint)); - } - catch (ObjectDisposedException) - { - OnListenerStopped(this, new ConnectionListenerStoppedEventArgs(LocalEndPoint)); - } - catch (Exception ex) - { - OnListenerStopped(this, - new ConnectionListenerStoppedEventArgs(LocalEndPoint, _cancellationPending ? null : ex)); - } - finally - { - _backgroundWorkerTask = null; - _cancellationPending = false; - } - } - - #endregion - } + continue; + } + + OnConnectionAccepted(this, new ConnectionAcceptedEventArgs(client)); + } catch(Exception ex) { + OnConnectionFailure(this, new ConnectionFailureEventArgs(ex)); + } + } + + OnListenerStopped(this, new ConnectionListenerStoppedEventArgs(this.LocalEndPoint)); + } catch(ObjectDisposedException) { + OnListenerStopped(this, new ConnectionListenerStoppedEventArgs(this.LocalEndPoint)); + } catch(Exception ex) { + OnListenerStopped(this, + new ConnectionListenerStoppedEventArgs(this.LocalEndPoint, this._cancellationPending ? null : ex)); + } finally { + this._backgroundWorkerTask = null; + this._cancellationPending = false; + } + } + + #endregion + } } diff --git a/Unosquare.Swan/Networking/DnsClient.Interfaces.cs b/Unosquare.Swan/Networking/DnsClient.Interfaces.cs index ae7fe78..5ca31ff 100644 --- a/Unosquare.Swan/Networking/DnsClient.Interfaces.cs +++ b/Unosquare.Swan/Networking/DnsClient.Interfaces.cs @@ -1,61 +1,95 @@ -namespace Unosquare.Swan.Networking -{ - using System; - using System.Collections.Generic; - - /// - /// DnsClient public interfaces. - /// - internal partial class DnsClient - { - public interface IDnsMessage - { - IList Questions { get; } - - int Size { get; } - byte[] ToArray(); - } - - public interface IDnsMessageEntry - { - DnsDomain Name { get; } - DnsRecordType Type { get; } - DnsRecordClass Class { get; } - - int Size { get; } - byte[] ToArray(); - } - - public interface IDnsResourceRecord : IDnsMessageEntry - { - TimeSpan TimeToLive { get; } - int DataLength { get; } - byte[] Data { get; } - } - - public interface IDnsRequest : IDnsMessage - { - int Id { get; set; } - DnsOperationCode OperationCode { get; set; } - bool RecursionDesired { get; set; } - } - - public interface IDnsResponse : IDnsMessage - { - int Id { get; set; } - IList AnswerRecords { get; } - IList AuthorityRecords { get; } - IList AdditionalRecords { get; } - bool IsRecursionAvailable { get; set; } - bool IsAuthorativeServer { get; set; } - bool IsTruncated { get; set; } - DnsOperationCode OperationCode { get; set; } - DnsResponseCode ResponseCode { get; set; } - } - - public interface IDnsRequestResolver - { - DnsClientResponse Request(DnsClientRequest request); - } - } +using System; +using System.Collections.Generic; + +namespace Unosquare.Swan.Networking { + /// + /// DnsClient public interfaces. + /// + internal partial class DnsClient { + public interface IDnsMessage { + IList Questions { + get; + } + + Int32 Size { + get; + } + Byte[] ToArray(); + } + + public interface IDnsMessageEntry { + DnsDomain Name { + get; + } + DnsRecordType Type { + get; + } + DnsRecordClass Class { + get; + } + + Int32 Size { + get; + } + Byte[] ToArray(); + } + + public interface IDnsResourceRecord : IDnsMessageEntry { + TimeSpan TimeToLive { + get; + } + Int32 DataLength { + get; + } + Byte[] Data { + get; + } + } + + public interface IDnsRequest : IDnsMessage { + Int32 Id { + get; set; + } + DnsOperationCode OperationCode { + get; set; + } + Boolean RecursionDesired { + get; set; + } + } + + public interface IDnsResponse : IDnsMessage { + Int32 Id { + get; set; + } + IList AnswerRecords { + get; + } + IList AuthorityRecords { + get; + } + IList AdditionalRecords { + get; + } + Boolean IsRecursionAvailable { + get; set; + } + Boolean IsAuthorativeServer { + get; set; + } + Boolean IsTruncated { + get; set; + } + DnsOperationCode OperationCode { + get; set; + } + DnsResponseCode ResponseCode { + get; set; + } + } + + public interface IDnsRequestResolver { + DnsClientResponse Request(DnsClientRequest request); + } + } } diff --git a/Unosquare.Swan/Networking/DnsClient.Request.cs b/Unosquare.Swan/Networking/DnsClient.Request.cs index d129c24..605ae3d 100644 --- a/Unosquare.Swan/Networking/DnsClient.Request.cs +++ b/Unosquare.Swan/Networking/DnsClient.Request.cs @@ -1,683 +1,582 @@ -namespace Unosquare.Swan.Networking -{ - using Formatters; - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Net; - using System.Net.Sockets; - using System.Runtime.InteropServices; - using System.Text; - using Exceptions; - using Attributes; - - /// - /// DnsClient Request inner class. - /// - internal partial class DnsClient - { - public class DnsClientRequest : IDnsRequest - { - private readonly IDnsRequestResolver _resolver; - private readonly IDnsRequest _request; - - public DnsClientRequest(IPEndPoint dns, IDnsRequest request = null, IDnsRequestResolver resolver = null) - { - Dns = dns; - _request = request == null ? new DnsRequest() : new DnsRequest(request); - _resolver = resolver ?? new DnsUdpRequestResolver(); - } - - public int Id - { - get => _request.Id; - set => _request.Id = value; - } - - public DnsOperationCode OperationCode - { - get => _request.OperationCode; - set => _request.OperationCode = value; - } - - public bool RecursionDesired - { - get => _request.RecursionDesired; - set => _request.RecursionDesired = value; - } - - public IList Questions => _request.Questions; - - public int Size => _request.Size; - - public IPEndPoint Dns { get; set; } - - public byte[] ToArray() => _request.ToArray(); - - public override string ToString() => _request.ToString(); - - /// - /// Resolves this request into a response using the provided DNS information. The given - /// request strategy is used to retrieve the response. - /// - /// Throw if a malformed response is received from the server. - /// Thrown if a IO error occurs. - /// Thrown if a the reading or writing to the socket fails. - /// The response received from server. - public DnsClientResponse Resolve() - { - try - { - var response = _resolver.Request(this); - - if (response.Id != Id) - { - throw new DnsQueryException(response, "Mismatching request/response IDs"); - } - - if (response.ResponseCode != DnsResponseCode.NoError) - { - throw new DnsQueryException(response); - } - - return response; - } - catch (ArgumentException e) - { - throw new DnsQueryException("Invalid response", e); - } - } - } - - public class DnsRequest : IDnsRequest - { - private static readonly Random Random = new Random(); - - private readonly IList questions; - private DnsHeader header; - - public DnsRequest() - { - questions = new List(); - header = new DnsHeader - { - OperationCode = DnsOperationCode.Query, - Response = false, - Id = Random.Next(UInt16.MaxValue), - }; - } - - public DnsRequest(IDnsRequest request) - { - header = new DnsHeader(); - questions = new List(request.Questions); - - header.Response = false; - - Id = request.Id; - OperationCode = request.OperationCode; - RecursionDesired = request.RecursionDesired; - } - - public IList Questions => questions; - - public int Size => header.Size + questions.Sum(q => q.Size); - - public int Id - { - get => header.Id; - set => header.Id = value; - } - - public DnsOperationCode OperationCode - { - get => header.OperationCode; - set => header.OperationCode = value; - } - - public bool RecursionDesired - { - get => header.RecursionDesired; - set => header.RecursionDesired = value; - } - - public byte[] ToArray() - { - UpdateHeader(); - var result = new MemoryStream(Size); - - result - .Append(header.ToArray()) - .Append(questions.Select(q => q.ToArray())); - - return result.ToArray(); - } - - public override string ToString() - { - UpdateHeader(); - - return Json.Serialize(this, true); - } - - private void UpdateHeader() - { - header.QuestionCount = questions.Count; - } - } - - public class DnsTcpRequestResolver : IDnsRequestResolver - { - public DnsClientResponse Request(DnsClientRequest request) - { - var tcp = new TcpClient(); - - try - { - tcp.Client.Connect(request.Dns); - - var stream = tcp.GetStream(); - var buffer = request.ToArray(); - var length = BitConverter.GetBytes((ushort) buffer.Length); - - if (BitConverter.IsLittleEndian) - Array.Reverse(length); - - stream.Write(length, 0, length.Length); - stream.Write(buffer, 0, buffer.Length); - - buffer = new byte[2]; - Read(stream, buffer); - - if (BitConverter.IsLittleEndian) - Array.Reverse(buffer); - - buffer = new byte[BitConverter.ToUInt16(buffer, 0)]; - Read(stream, buffer); - - var response = DnsResponse.FromArray(buffer); - - return new DnsClientResponse(request, response, buffer); - } - finally - { +using Unosquare.Swan.Formatters; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Text; +using Unosquare.Swan.Exceptions; +using Unosquare.Swan.Attributes; + +namespace Unosquare.Swan.Networking { + /// + /// DnsClient Request inner class. + /// + internal partial class DnsClient { + public class DnsClientRequest : IDnsRequest { + private readonly IDnsRequestResolver _resolver; + private readonly IDnsRequest _request; + + public DnsClientRequest(IPEndPoint dns, IDnsRequest request = null, IDnsRequestResolver resolver = null) { + this.Dns = dns; + this._request = request == null ? new DnsRequest() : new DnsRequest(request); + this._resolver = resolver ?? new DnsUdpRequestResolver(); + } + + public Int32 Id { + get => this._request.Id; + set => this._request.Id = value; + } + + public DnsOperationCode OperationCode { + get => this._request.OperationCode; + set => this._request.OperationCode = value; + } + + public Boolean RecursionDesired { + get => this._request.RecursionDesired; + set => this._request.RecursionDesired = value; + } + + public IList Questions => this._request.Questions; + + public Int32 Size => this._request.Size; + + public IPEndPoint Dns { + get; set; + } + + public Byte[] ToArray() => this._request.ToArray(); + + public override String ToString() => this._request.ToString(); + + /// + /// Resolves this request into a response using the provided DNS information. The given + /// request strategy is used to retrieve the response. + /// + /// Throw if a malformed response is received from the server. + /// Thrown if a IO error occurs. + /// Thrown if a the reading or writing to the socket fails. + /// The response received from server. + public DnsClientResponse Resolve() { + try { + DnsClientResponse response = this._resolver.Request(this); + + if(response.Id != this.Id) { + throw new DnsQueryException(response, "Mismatching request/response IDs"); + } + + if(response.ResponseCode != DnsResponseCode.NoError) { + throw new DnsQueryException(response); + } + + return response; + } catch(ArgumentException e) { + throw new DnsQueryException("Invalid response", e); + } + } + } + + public class DnsRequest : IDnsRequest { + private static readonly Random Random = new Random(); + private DnsHeader header; + + public DnsRequest() { + this.Questions = new List(); + this.header = new DnsHeader { + OperationCode = DnsOperationCode.Query, + Response = false, + Id = Random.Next(UInt16.MaxValue), + }; + } + + public DnsRequest(IDnsRequest request) { + this.header = new DnsHeader(); + this.Questions = new List(request.Questions); + + this.header.Response = false; + + this.Id = request.Id; + this.OperationCode = request.OperationCode; + this.RecursionDesired = request.RecursionDesired; + } + + public IList Questions { + get; + } + + public Int32 Size => this.header.Size + this.Questions.Sum(q => q.Size); + + public Int32 Id { + get => this.header.Id; + set => this.header.Id = value; + } + + public DnsOperationCode OperationCode { + get => this.header.OperationCode; + set => this.header.OperationCode = value; + } + + public Boolean RecursionDesired { + get => this.header.RecursionDesired; + set => this.header.RecursionDesired = value; + } + + public Byte[] ToArray() { + this.UpdateHeader(); + MemoryStream result = new MemoryStream(this.Size); + + _ = result + .Append(this.header.ToArray()) + .Append(this.Questions.Select(q => q.ToArray())); + + return result.ToArray(); + } + + public override String ToString() { + this.UpdateHeader(); + + return Json.Serialize(this, true); + } + + private void UpdateHeader() => this.header.QuestionCount = this.Questions.Count; + } + + public class DnsTcpRequestResolver : IDnsRequestResolver { + public DnsClientResponse Request(DnsClientRequest request) { + TcpClient tcp = new TcpClient(); + + try { + tcp.Client.Connect(request.Dns); + + NetworkStream stream = tcp.GetStream(); + Byte[] buffer = request.ToArray(); + Byte[] length = BitConverter.GetBytes((UInt16)buffer.Length); + + if(BitConverter.IsLittleEndian) { + Array.Reverse(length); + } + + stream.Write(length, 0, length.Length); + stream.Write(buffer, 0, buffer.Length); + + buffer = new Byte[2]; + Read(stream, buffer); + + if(BitConverter.IsLittleEndian) { + Array.Reverse(buffer); + } + + buffer = new Byte[BitConverter.ToUInt16(buffer, 0)]; + Read(stream, buffer); + + DnsResponse response = DnsResponse.FromArray(buffer); + + return new DnsClientResponse(request, response, buffer); + } finally { #if NET452 - tcp.Close(); + tcp.Close(); #else tcp.Dispose(); #endif - } - } - - private static void Read(Stream stream, byte[] buffer) - { - var length = buffer.Length; - var offset = 0; - int size; - - while (length > 0 && (size = stream.Read(buffer, offset, length)) > 0) - { - offset += size; - length -= size; - } - - if (length > 0) - { - throw new IOException("Unexpected end of stream"); - } - } - } - - public class DnsUdpRequestResolver : IDnsRequestResolver - { - private readonly IDnsRequestResolver _fallback; - - public DnsUdpRequestResolver(IDnsRequestResolver fallback) - { - _fallback = fallback; - } - - public DnsUdpRequestResolver() - { - _fallback = new DnsNullRequestResolver(); - } - - public DnsClientResponse Request(DnsClientRequest request) - { - var udp = new UdpClient(); - var dns = request.Dns; - - try - { - udp.Client.SendTimeout = 7000; - udp.Client.ReceiveTimeout = 7000; - udp.Client.Connect(dns); - udp.Client.Send(request.ToArray()); - - var bufferList = new List(); - - do - { - var tempBuffer = new byte[1024]; - var receiveCount = udp.Client.Receive(tempBuffer); - bufferList.AddRange(tempBuffer.Skip(0).Take(receiveCount)); - } while (udp.Client.Available > 0 || bufferList.Count == 0); - - var buffer = bufferList.ToArray(); - var response = DnsResponse.FromArray(buffer); - - return response.IsTruncated - ? _fallback.Request(request) - : new DnsClientResponse(request, response, buffer); - } - finally - { + } + } + + private static void Read(Stream stream, Byte[] buffer) { + Int32 length = buffer.Length; + Int32 offset = 0; + Int32 size; + + while(length > 0 && (size = stream.Read(buffer, offset, length)) > 0) { + offset += size; + length -= size; + } + + if(length > 0) { + throw new IOException("Unexpected end of stream"); + } + } + } + + public class DnsUdpRequestResolver : IDnsRequestResolver { + private readonly IDnsRequestResolver _fallback; + + public DnsUdpRequestResolver(IDnsRequestResolver fallback) => this._fallback = fallback; + + public DnsUdpRequestResolver() => this._fallback = new DnsNullRequestResolver(); + + public DnsClientResponse Request(DnsClientRequest request) { + UdpClient udp = new UdpClient(); + IPEndPoint dns = request.Dns; + + try { + udp.Client.SendTimeout = 7000; + udp.Client.ReceiveTimeout = 7000; + udp.Client.Connect(dns); + udp.Client.Send(request.ToArray()); + + List bufferList = new List(); + + do { + Byte[] tempBuffer = new Byte[1024]; + Int32 receiveCount = udp.Client.Receive(tempBuffer); + bufferList.AddRange(tempBuffer.Skip(0).Take(receiveCount)); + } while(udp.Client.Available > 0 || bufferList.Count == 0); + + Byte[] buffer = bufferList.ToArray(); + DnsResponse response = DnsResponse.FromArray(buffer); + + return response.IsTruncated + ? this._fallback.Request(request) + : new DnsClientResponse(request, response, buffer); + } finally { #if NET452 - udp.Close(); + udp.Close(); #else udp.Dispose(); #endif - } - } - } - - public class DnsNullRequestResolver : IDnsRequestResolver - { - public DnsClientResponse Request(DnsClientRequest request) - { - throw new DnsQueryException("Request failed"); - } - } - - // 12 bytes message header - [StructEndianness(Endianness.Big)] - [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct DnsHeader - { - public const int SIZE = 12; - - public static DnsHeader FromArray(byte[] header) - { - if (header.Length < SIZE) - { - throw new ArgumentException("Header length too small"); - } - - return header.ToStruct(0, SIZE); - } - - private ushort id; - - private byte flag0; - private byte flag1; - - // Question count: number of questions in the Question section - private ushort questionCount; - - // Answer record count: number of records in the Answer section - private ushort answerCount; - - // Authority record count: number of records in the Authority section - private ushort authorityCount; - - // Additional record count: number of records in the Additional section - private ushort addtionalCount; - - public int Id - { - get => id; - set => id = (ushort) value; - } - - public int QuestionCount - { - get => questionCount; - set => questionCount = (ushort) value; - } - - public int AnswerRecordCount - { - get => answerCount; - set => answerCount = (ushort) value; - } - - public int AuthorityRecordCount - { - get => authorityCount; - set => authorityCount = (ushort) value; - } - - public int AdditionalRecordCount - { - get => addtionalCount; - set => addtionalCount = (ushort) value; - } - - public bool Response - { - get => Qr == 1; - set => Qr = Convert.ToByte(value); - } - - public DnsOperationCode OperationCode - { - get => (DnsOperationCode) Opcode; - set => Opcode = (byte) value; - } - - public bool AuthorativeServer - { - get => Aa == 1; - set => Aa = Convert.ToByte(value); - } - - public bool Truncated - { - get => Tc == 1; - set => Tc = Convert.ToByte(value); - } - - public bool RecursionDesired - { - get => Rd == 1; - set => Rd = Convert.ToByte(value); - } - - public bool RecursionAvailable - { - get => Ra == 1; - set => Ra = Convert.ToByte(value); - } - - public DnsResponseCode ResponseCode - { - get => (DnsResponseCode) RCode; - set => RCode = (byte) value; - } - - public int Size => SIZE; - - // Query/Response Flag - private byte Qr - { - get => Flag0.GetBitValueAt(7); - set => Flag0 = Flag0.SetBitValueAt(7, 1, value); - } - - // Operation Code - private byte Opcode - { - get => Flag0.GetBitValueAt(3, 4); - set => Flag0 = Flag0.SetBitValueAt(3, 4, value); - } - - // Authorative Answer Flag - private byte Aa - { - get => Flag0.GetBitValueAt(2); - set => Flag0 = Flag0.SetBitValueAt(2, 1, value); - } - - // Truncation Flag - private byte Tc - { - get => Flag0.GetBitValueAt(1); - set => Flag0 = Flag0.SetBitValueAt(1, 1, value); - } - - // Recursion Desired - private byte Rd - { - get => Flag0.GetBitValueAt(0); - set => Flag0 = Flag0.SetBitValueAt(0, 1, value); - } - - // Recursion Available - private byte Ra - { - get => Flag1.GetBitValueAt(7); - set => Flag1 = Flag1.SetBitValueAt(7, 1, value); - } - - // Zero (Reserved) - private byte Z - { - get => Flag1.GetBitValueAt(4, 3); - set { } - } - - // Response Code - private byte RCode - { - get => Flag1.GetBitValueAt(0, 4); - set => Flag1 = Flag1.SetBitValueAt(0, 4, value); - } - - private byte Flag0 - { - get => flag0; - set => flag0 = value; - } - - private byte Flag1 - { - get => flag1; - set => flag1 = value; - } - - public byte[] ToArray() => this.ToBytes(); - - public override string ToString() - => Json.SerializeExcluding(this, true, nameof(Size)); - } - - public class DnsDomain : IComparable - { - private readonly string[] _labels; - - public DnsDomain(string domain) - : this(domain.Split('.')) - { - } - - public DnsDomain(string[] labels) - { - _labels = labels; - } - - public int Size => _labels.Sum(l => l.Length) + _labels.Length + 1; - - public static DnsDomain FromArray(byte[] message, int offset) - => FromArray(message, offset, out offset); - - public static DnsDomain FromArray(byte[] message, int offset, out int endOffset) - { - var labels = new List(); - var endOffsetAssigned = false; - endOffset = 0; - byte lengthOrPointer; - - while ((lengthOrPointer = message[offset++]) > 0) - { - // Two heighest bits are set (pointer) - if (lengthOrPointer.GetBitValueAt(6, 2) == 3) - { - if (!endOffsetAssigned) - { - endOffsetAssigned = true; - endOffset = offset + 1; - } - - ushort pointer = lengthOrPointer.GetBitValueAt(0, 6); - offset = (pointer << 8) | message[offset]; - - continue; - } - - if (lengthOrPointer.GetBitValueAt(6, 2) != 0) - { - throw new ArgumentException("Unexpected bit pattern in label length"); - } - - var length = lengthOrPointer; - var label = new byte[length]; - Array.Copy(message, offset, label, 0, length); - - labels.Add(label); - - offset += length; - } - - if (!endOffsetAssigned) - { - endOffset = offset; - } - - return new DnsDomain(labels.Select(l => l.ToText(Encoding.ASCII)).ToArray()); - } - - public static DnsDomain PointerName(IPAddress ip) - => new DnsDomain(FormatReverseIP(ip)); - - public byte[] ToArray() - { - var result = new byte[Size]; - var offset = 0; - - foreach (var l in _labels.Select(label => Encoding.ASCII.GetBytes(label))) - { - result[offset++] = (byte) l.Length; - l.CopyTo(result, offset); - - offset += l.Length; - } - - result[offset] = 0; - - return result; - } - - public override string ToString() - => string.Join(".", _labels); - - public int CompareTo(DnsDomain other) - => string.Compare(ToString(), other.ToString(), StringComparison.Ordinal); - - public override bool Equals(object obj) - => obj is DnsDomain domain && CompareTo(domain) == 0; - - public override int GetHashCode() => ToString().GetHashCode(); - - private static string FormatReverseIP(IPAddress ip) - { - var address = ip.GetAddressBytes(); - - if (address.Length == 4) - { - return string.Join(".", address.Reverse().Select(b => b.ToString())) + ".in-addr.arpa"; - } - - var nibbles = new byte[address.Length * 2]; - - for (int i = 0, j = 0; i < address.Length; i++, j = 2 * i) - { - var b = address[i]; - - nibbles[j] = b.GetBitValueAt(4, 4); - nibbles[j + 1] = b.GetBitValueAt(0, 4); - } - - return string.Join(".", nibbles.Reverse().Select(b => b.ToString("x"))) + ".ip6.arpa"; - } - } - - public class DnsQuestion : IDnsMessageEntry - { - private readonly DnsDomain _domain; - private readonly DnsRecordType _type; - private readonly DnsRecordClass _klass; - - public static IList GetAllFromArray(byte[] message, int offset, int questionCount) => - GetAllFromArray(message, offset, questionCount, out offset); - - public static IList GetAllFromArray( - byte[] message, - int offset, - int questionCount, - out int endOffset) - { - IList questions = new List(questionCount); - - for (var i = 0; i < questionCount; i++) - { - questions.Add(FromArray(message, offset, out offset)); - } - - endOffset = offset; - return questions; - } - - public static DnsQuestion FromArray(byte[] message, int offset, out int endOffset) - { - var domain = DnsDomain.FromArray(message, offset, out offset); - var tail = message.ToStruct(offset, Tail.SIZE); - - endOffset = offset + Tail.SIZE; - - return new DnsQuestion(domain, tail.Type, tail.Class); - } - - public DnsQuestion( - DnsDomain domain, - DnsRecordType type = DnsRecordType.A, - DnsRecordClass klass = DnsRecordClass.IN) - { - _domain = domain; - _type = type; - _klass = klass; - } - - public DnsDomain Name => _domain; - - public DnsRecordType Type => _type; - - public DnsRecordClass Class => _klass; - - public int Size => _domain.Size + Tail.SIZE; - - public byte[] ToArray() - { - return new MemoryStream(Size) - .Append(_domain.ToArray()) - .Append(new Tail {Type = Type, Class = Class}.ToBytes()) - .ToArray(); - } - - public override string ToString() - => Json.SerializeOnly(this, true, nameof(Name), nameof(Type), nameof(Class)); - - [StructEndianness(Endianness.Big)] - [StructLayout(LayoutKind.Sequential, Pack = 2)] - private struct Tail - { - public const int SIZE = 4; - - private ushort type; - private ushort klass; - - public DnsRecordType Type - { - get => (DnsRecordType) type; - set => type = (ushort) value; - } - - public DnsRecordClass Class - { - get => (DnsRecordClass) klass; - set => klass = (ushort) value; - } - } - } - } + } + } + } + + public class DnsNullRequestResolver : IDnsRequestResolver { + public DnsClientResponse Request(DnsClientRequest request) => throw new DnsQueryException("Request failed"); + } + + // 12 bytes message header + [StructEndianness(Endianness.Big)] + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct DnsHeader { + public const Int32 SIZE = 12; + + public static DnsHeader FromArray(Byte[] header) { + if(header.Length < SIZE) { + throw new ArgumentException("Header length too small"); + } + + return header.ToStruct(0, SIZE); + } + + private UInt16 id; + + // Question count: number of questions in the Question section + private UInt16 questionCount; + + // Answer record count: number of records in the Answer section + private UInt16 answerCount; + + // Authority record count: number of records in the Authority section + private UInt16 authorityCount; + + // Additional record count: number of records in the Additional section + private UInt16 addtionalCount; + + public Int32 Id { + get => this.id; + set => this.id = (UInt16)value; + } + + public Int32 QuestionCount { + get => this.questionCount; + set => this.questionCount = (UInt16)value; + } + + public Int32 AnswerRecordCount { + get => this.answerCount; + set => this.answerCount = (UInt16)value; + } + + public Int32 AuthorityRecordCount { + get => this.authorityCount; + set => this.authorityCount = (UInt16)value; + } + + public Int32 AdditionalRecordCount { + get => this.addtionalCount; + set => this.addtionalCount = (UInt16)value; + } + + public Boolean Response { + get => this.Qr == 1; + set => this.Qr = Convert.ToByte(value); + } + + public DnsOperationCode OperationCode { + get => (DnsOperationCode)this.Opcode; + set => this.Opcode = (Byte)value; + } + + public Boolean AuthorativeServer { + get => this.Aa == 1; + set => this.Aa = Convert.ToByte(value); + } + + public Boolean Truncated { + get => this.Tc == 1; + set => this.Tc = Convert.ToByte(value); + } + + public Boolean RecursionDesired { + get => this.Rd == 1; + set => this.Rd = Convert.ToByte(value); + } + + public Boolean RecursionAvailable { + get => this.Ra == 1; + set => this.Ra = Convert.ToByte(value); + } + + public DnsResponseCode ResponseCode { + get => (DnsResponseCode)this.RCode; + set => this.RCode = (Byte)value; + } + + public Int32 Size => SIZE; + + // Query/Response Flag + private Byte Qr { + get => this.Flag0.GetBitValueAt(7); + set => this.Flag0 = this.Flag0.SetBitValueAt(7, 1, value); + } + + // Operation Code + private Byte Opcode { + get => this.Flag0.GetBitValueAt(3, 4); + set => this.Flag0 = this.Flag0.SetBitValueAt(3, 4, value); + } + + // Authorative Answer Flag + private Byte Aa { + get => this.Flag0.GetBitValueAt(2); + set => this.Flag0 = this.Flag0.SetBitValueAt(2, 1, value); + } + + // Truncation Flag + private Byte Tc { + get => this.Flag0.GetBitValueAt(1); + set => this.Flag0 = this.Flag0.SetBitValueAt(1, 1, value); + } + + // Recursion Desired + private Byte Rd { + get => this.Flag0.GetBitValueAt(0); + set => this.Flag0 = this.Flag0.SetBitValueAt(0, 1, value); + } + + // Recursion Available + private Byte Ra { + get => this.Flag1.GetBitValueAt(7); + set => this.Flag1 = this.Flag1.SetBitValueAt(7, 1, value); + } + + // Zero (Reserved) + private Byte Z { + get => this.Flag1.GetBitValueAt(4, 3); + set { + } + } + + // Response Code + private Byte RCode { + get => this.Flag1.GetBitValueAt(0, 4); + set => this.Flag1 = this.Flag1.SetBitValueAt(0, 4, value); + } + + private Byte Flag0 { get; + set; } + + private Byte Flag1 { get; + set; } + + public Byte[] ToArray() => this.ToBytes(); + + public override String ToString() + => Json.SerializeExcluding(this, true, nameof(this.Size)); + } + + public class DnsDomain : IComparable { + private readonly String[] _labels; + + public DnsDomain(String domain) + : this(domain.Split('.')) { + } + + public DnsDomain(String[] labels) => this._labels = labels; + + public Int32 Size => this._labels.Sum(l => l.Length) + this._labels.Length + 1; + + public static DnsDomain FromArray(Byte[] message, Int32 offset) + => FromArray(message, offset, out _); + + public static DnsDomain FromArray(Byte[] message, Int32 offset, out Int32 endOffset) { + List labels = new List(); + Boolean endOffsetAssigned = false; + endOffset = 0; + Byte lengthOrPointer; + + while((lengthOrPointer = message[offset++]) > 0) { + // Two heighest bits are set (pointer) + if(lengthOrPointer.GetBitValueAt(6, 2) == 3) { + if(!endOffsetAssigned) { + endOffsetAssigned = true; + endOffset = offset + 1; + } + + UInt16 pointer = lengthOrPointer.GetBitValueAt(0, 6); + offset = (pointer << 8) | message[offset]; + + continue; + } + + if(lengthOrPointer.GetBitValueAt(6, 2) != 0) { + throw new ArgumentException("Unexpected bit pattern in label length"); + } + + Byte length = lengthOrPointer; + Byte[] label = new Byte[length]; + Array.Copy(message, offset, label, 0, length); + + labels.Add(label); + + offset += length; + } + + if(!endOffsetAssigned) { + endOffset = offset; + } + + return new DnsDomain(labels.Select(l => l.ToText(Encoding.ASCII)).ToArray()); + } + + public static DnsDomain PointerName(IPAddress ip) + => new DnsDomain(FormatReverseIP(ip)); + + public Byte[] ToArray() { + Byte[] result = new Byte[this.Size]; + Int32 offset = 0; + + foreach(Byte[] l in this._labels.Select(label => Encoding.ASCII.GetBytes(label))) { + result[offset++] = (Byte)l.Length; + l.CopyTo(result, offset); + + offset += l.Length; + } + + result[offset] = 0; + + return result; + } + + public override String ToString() + => String.Join(".", this._labels); + + public Int32 CompareTo(DnsDomain other) + => String.Compare(this.ToString(), other.ToString(), StringComparison.Ordinal); + + public override Boolean Equals(Object obj) + => obj is DnsDomain domain && this.CompareTo(domain) == 0; + + public override Int32 GetHashCode() => this.ToString().GetHashCode(); + + private static String FormatReverseIP(IPAddress ip) { + Byte[] address = ip.GetAddressBytes(); + + if(address.Length == 4) { + return String.Join(".", address.Reverse().Select(b => b.ToString())) + ".in-addr.arpa"; + } + + Byte[] nibbles = new Byte[address.Length * 2]; + + for(Int32 i = 0, j = 0; i < address.Length; i++, j = 2 * i) { + Byte b = address[i]; + + nibbles[j] = b.GetBitValueAt(4, 4); + nibbles[j + 1] = b.GetBitValueAt(0, 4); + } + + return String.Join(".", nibbles.Reverse().Select(b => b.ToString("x"))) + ".ip6.arpa"; + } + } + + public class DnsQuestion : IDnsMessageEntry { + public static IList GetAllFromArray(Byte[] message, Int32 offset, Int32 questionCount) => + GetAllFromArray(message, offset, questionCount, out _); + + public static IList GetAllFromArray( + Byte[] message, + Int32 offset, + Int32 questionCount, + out Int32 endOffset) { + IList questions = new List(questionCount); + + for(Int32 i = 0; i < questionCount; i++) { + questions.Add(FromArray(message, offset, out offset)); + } + + endOffset = offset; + return questions; + } + + public static DnsQuestion FromArray(Byte[] message, Int32 offset, out Int32 endOffset) { + DnsDomain domain = DnsDomain.FromArray(message, offset, out offset); + Tail tail = message.ToStruct(offset, Tail.SIZE); + + endOffset = offset + Tail.SIZE; + + return new DnsQuestion(domain, tail.Type, tail.Class); + } + + public DnsQuestion( + DnsDomain domain, + DnsRecordType type = DnsRecordType.A, + DnsRecordClass klass = DnsRecordClass.IN) { + this.Name = domain; + this.Type = type; + this.Class = klass; + } + + public DnsDomain Name { + get; + } + + public DnsRecordType Type { + get; + } + + public DnsRecordClass Class { + get; + } + + public Int32 Size => this.Name.Size + Tail.SIZE; + + public Byte[] ToArray() => new MemoryStream(this.Size) + .Append(this.Name.ToArray()) + .Append(new Tail { Type = Type, Class = Class }.ToBytes()) + .ToArray(); + + public override String ToString() + => Json.SerializeOnly(this, true, nameof(this.Name), nameof(this.Type), nameof(this.Class)); + + [StructEndianness(Endianness.Big)] + [StructLayout(LayoutKind.Sequential, Pack = 2)] + private struct Tail { + public const Int32 SIZE = 4; + + private UInt16 type; + private UInt16 klass; + + public DnsRecordType Type { + get => (DnsRecordType)this.type; + set => this.type = (UInt16)value; + } + + public DnsRecordClass Class { + get => (DnsRecordClass)this.klass; + set => this.klass = (UInt16)value; + } + } + } + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/DnsClient.ResourceRecords.cs b/Unosquare.Swan/Networking/DnsClient.ResourceRecords.cs index 4a43f30..579dc1c 100644 --- a/Unosquare.Swan/Networking/DnsClient.ResourceRecords.cs +++ b/Unosquare.Swan/Networking/DnsClient.ResourceRecords.cs @@ -1,460 +1,432 @@ -namespace Unosquare.Swan.Networking -{ - using Attributes; - using Formatters; - using System; - using System.Collections.Generic; - using System.IO; - using System.Net; - using System.Runtime.InteropServices; - - /// - /// DnsClient public methods. - /// - internal partial class DnsClient - { - public abstract class DnsResourceRecordBase : IDnsResourceRecord - { - private readonly IDnsResourceRecord _record; - - protected DnsResourceRecordBase(IDnsResourceRecord record) - { - _record = record; - } - - public DnsDomain Name => _record.Name; - - public DnsRecordType Type => _record.Type; - - public DnsRecordClass Class => _record.Class; - - public TimeSpan TimeToLive => _record.TimeToLive; - - public int DataLength => _record.DataLength; - - public byte[] Data => _record.Data; - - public int Size => _record.Size; - - protected virtual string[] IncludedProperties - => new[] {nameof(Name), nameof(Type), nameof(Class), nameof(TimeToLive), nameof(DataLength)}; - - public byte[] ToArray() => _record.ToArray(); - - public override string ToString() - => Json.SerializeOnly(this, true, IncludedProperties); - } - - public class DnsResourceRecord : IDnsResourceRecord - { - public DnsResourceRecord( - DnsDomain domain, - byte[] data, - DnsRecordType type, - DnsRecordClass klass = DnsRecordClass.IN, - TimeSpan ttl = default) - { - Name = domain; - Type = type; - Class = klass; - TimeToLive = ttl; - Data = data; - } - - public DnsDomain Name { get; } - - public DnsRecordType Type { get; } - - public DnsRecordClass Class { get; } - - public TimeSpan TimeToLive { get; } - - public int DataLength => Data.Length; - - public byte[] Data { get; } - - public int Size => Name.Size + Tail.SIZE + Data.Length; - - public static IList GetAllFromArray( - byte[] message, - int offset, - int count, - out int endOffset) - { - IList records = new List(count); - - for (var i = 0; i < count; i++) - { - records.Add(FromArray(message, offset, out offset)); - } - - endOffset = offset; - return records; - } - - public static DnsResourceRecord FromArray(byte[] message, int offset, out int endOffset) - { - var domain = DnsDomain.FromArray(message, offset, out offset); - var tail = message.ToStruct(offset, Tail.SIZE); - - var data = new byte[tail.DataLength]; - - offset += Tail.SIZE; - Array.Copy(message, offset, data, 0, data.Length); - - endOffset = offset + data.Length; - - return new DnsResourceRecord(domain, data, tail.Type, tail.Class, tail.TimeToLive); - } - - public byte[] ToArray() - { - return new MemoryStream(Size) - .Append(Name.ToArray()) - .Append(new Tail() - { - Type = Type, - Class = Class, - TimeToLive = TimeToLive, - DataLength = Data.Length, - }.ToBytes()) - .Append(Data) - .ToArray(); - } - - public override string ToString() - { - return Json.SerializeOnly( - this, - true, - nameof(Name), - nameof(Type), - nameof(Class), - nameof(TimeToLive), - nameof(DataLength)); - } - - [StructEndianness(Endianness.Big)] - [StructLayout(LayoutKind.Sequential, Pack = 2)] - private struct Tail - { - public const int SIZE = 10; - - private ushort type; - private ushort klass; - private uint ttl; - private ushort dataLength; - - public DnsRecordType Type - { - get => (DnsRecordType) type; - set => type = (ushort) value; - } - - public DnsRecordClass Class - { - get => (DnsRecordClass) klass; - set => klass = (ushort) value; - } - - public TimeSpan TimeToLive - { - get => TimeSpan.FromSeconds(ttl); - set => ttl = (uint) value.TotalSeconds; - } - - public int DataLength - { - get => dataLength; - set => dataLength = (ushort) value; - } - } - } - - public class DnsPointerResourceRecord : DnsResourceRecordBase - { - public DnsPointerResourceRecord(IDnsResourceRecord record, byte[] message, int dataOffset) - : base(record) - { - PointerDomainName = DnsDomain.FromArray(message, dataOffset); - } - - public DnsDomain PointerDomainName { get; } - - protected override string[] IncludedProperties - { - get - { - var temp = new List(base.IncludedProperties) {nameof(PointerDomainName)}; - return temp.ToArray(); - } - } - } - - public class DnsIPAddressResourceRecord : DnsResourceRecordBase - { - public DnsIPAddressResourceRecord(IDnsResourceRecord record) - : base(record) - { - IPAddress = new IPAddress(Data); - } - - public IPAddress IPAddress { get; } - - protected override string[] IncludedProperties - { - get - { - var temp = new List(base.IncludedProperties) {nameof(IPAddress)}; - return temp.ToArray(); - } - } - } - - public class DnsNameServerResourceRecord : DnsResourceRecordBase - { - public DnsNameServerResourceRecord(IDnsResourceRecord record, byte[] message, int dataOffset) - : base(record) - { - NSDomainName = DnsDomain.FromArray(message, dataOffset); - } - - public DnsDomain NSDomainName { get; } - - protected override string[] IncludedProperties - { - get - { - var temp = new List(base.IncludedProperties) {nameof(NSDomainName)}; - return temp.ToArray(); - } - } - } - - public class DnsCanonicalNameResourceRecord : DnsResourceRecordBase - { - public DnsCanonicalNameResourceRecord(IDnsResourceRecord record, byte[] message, int dataOffset) - : base(record) - { - CanonicalDomainName = DnsDomain.FromArray(message, dataOffset); - } - - public DnsDomain CanonicalDomainName { get; } - - protected override string[] IncludedProperties => new List(base.IncludedProperties) - { - nameof(CanonicalDomainName), - }.ToArray(); - } - - public class DnsMailExchangeResourceRecord : DnsResourceRecordBase - { - private const int PreferenceSize = 2; - - public DnsMailExchangeResourceRecord( - IDnsResourceRecord record, - byte[] message, - int dataOffset) - : base(record) - { - var preference = new byte[PreferenceSize]; - Array.Copy(message, dataOffset, preference, 0, preference.Length); - - if (BitConverter.IsLittleEndian) - { - Array.Reverse(preference); - } - - dataOffset += PreferenceSize; - - Preference = BitConverter.ToUInt16(preference, 0); - ExchangeDomainName = DnsDomain.FromArray(message, dataOffset); - } - - public int Preference { get; } - - public DnsDomain ExchangeDomainName { get; } - - protected override string[] IncludedProperties => new List(base.IncludedProperties) - { - nameof(Preference), - nameof(ExchangeDomainName), - }.ToArray(); - } - - public class DnsStartOfAuthorityResourceRecord : DnsResourceRecordBase - { - public DnsStartOfAuthorityResourceRecord(IDnsResourceRecord record, byte[] message, int dataOffset) - : base(record) - { - MasterDomainName = DnsDomain.FromArray(message, dataOffset, out dataOffset); - ResponsibleDomainName = DnsDomain.FromArray(message, dataOffset, out dataOffset); - - var tail = message.ToStruct(dataOffset, Options.SIZE); - - SerialNumber = tail.SerialNumber; - RefreshInterval = tail.RefreshInterval; - RetryInterval = tail.RetryInterval; - ExpireInterval = tail.ExpireInterval; - MinimumTimeToLive = tail.MinimumTimeToLive; - } - - public DnsStartOfAuthorityResourceRecord( - DnsDomain domain, - DnsDomain master, - DnsDomain responsible, - long serial, - TimeSpan refresh, - TimeSpan retry, - TimeSpan expire, - TimeSpan minTtl, - TimeSpan ttl = default) - : base(Create(domain, master, responsible, serial, refresh, retry, expire, minTtl, ttl)) - { - MasterDomainName = master; - ResponsibleDomainName = responsible; - - SerialNumber = serial; - RefreshInterval = refresh; - RetryInterval = retry; - ExpireInterval = expire; - MinimumTimeToLive = minTtl; - } - - public DnsDomain MasterDomainName { get; } - - public DnsDomain ResponsibleDomainName { get; } - - public long SerialNumber { get; } - - public TimeSpan RefreshInterval { get; } - - public TimeSpan RetryInterval { get; } - - public TimeSpan ExpireInterval { get; } - - public TimeSpan MinimumTimeToLive { get; } - - protected override string[] IncludedProperties => new List(base.IncludedProperties) - { - nameof(MasterDomainName), - nameof(ResponsibleDomainName), - nameof(SerialNumber), - }.ToArray(); - - private static IDnsResourceRecord Create( - DnsDomain domain, - DnsDomain master, - DnsDomain responsible, - long serial, - TimeSpan refresh, - TimeSpan retry, - TimeSpan expire, - TimeSpan minTtl, - TimeSpan ttl) - { - var data = new MemoryStream(Options.SIZE + master.Size + responsible.Size); - var tail = new Options - { - SerialNumber = serial, - RefreshInterval = refresh, - RetryInterval = retry, - ExpireInterval = expire, - MinimumTimeToLive = minTtl, - }; - - data.Append(master.ToArray()).Append(responsible.ToArray()).Append(tail.ToBytes()); - - return new DnsResourceRecord(domain, data.ToArray(), DnsRecordType.SOA, DnsRecordClass.IN, ttl); - } - - [StructEndianness(Endianness.Big)] - [StructLayout(LayoutKind.Sequential, Pack = 4)] - public struct Options - { - public const int SIZE = 20; - - private uint serialNumber; - private uint refreshInterval; - private uint retryInterval; - private uint expireInterval; - private uint ttl; - - public long SerialNumber - { - get => serialNumber; - set => serialNumber = (uint) value; - } - - public TimeSpan RefreshInterval - { - get => TimeSpan.FromSeconds(refreshInterval); - set => refreshInterval = (uint) value.TotalSeconds; - } - - public TimeSpan RetryInterval - { - get => TimeSpan.FromSeconds(retryInterval); - set => retryInterval = (uint) value.TotalSeconds; - } - - public TimeSpan ExpireInterval - { - get => TimeSpan.FromSeconds(expireInterval); - set => expireInterval = (uint) value.TotalSeconds; - } - - public TimeSpan MinimumTimeToLive - { - get => TimeSpan.FromSeconds(ttl); - set => ttl = (uint) value.TotalSeconds; - } - } - } - - private static class DnsResourceRecordFactory - { - public static IList GetAllFromArray( - byte[] message, - int offset, - int count, - out int endOffset) - { - var result = new List(count); - - for (var i = 0; i < count; i++) - { - result.Add(GetFromArray(message, offset, out offset)); - } - - endOffset = offset; - return result; - } - - private static IDnsResourceRecord GetFromArray(byte[] message, int offset, out int endOffset) - { - var record = DnsResourceRecord.FromArray(message, offset, out endOffset); - var dataOffset = endOffset - record.DataLength; - - switch (record.Type) - { - case DnsRecordType.A: - case DnsRecordType.AAAA: - return new DnsIPAddressResourceRecord(record); - case DnsRecordType.NS: - return new DnsNameServerResourceRecord(record, message, dataOffset); - case DnsRecordType.CNAME: - return new DnsCanonicalNameResourceRecord(record, message, dataOffset); - case DnsRecordType.SOA: - return new DnsStartOfAuthorityResourceRecord(record, message, dataOffset); - case DnsRecordType.PTR: - return new DnsPointerResourceRecord(record, message, dataOffset); - case DnsRecordType.MX: - return new DnsMailExchangeResourceRecord(record, message, dataOffset); - default: - return record; - } - } - } - } +using Unosquare.Swan.Attributes; +using Unosquare.Swan.Formatters; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Runtime.InteropServices; + +namespace Unosquare.Swan.Networking { + /// + /// DnsClient public methods. + /// + internal partial class DnsClient { + public abstract class DnsResourceRecordBase : IDnsResourceRecord { + private readonly IDnsResourceRecord _record; + + protected DnsResourceRecordBase(IDnsResourceRecord record) => this._record = record; + + public DnsDomain Name => this._record.Name; + + public DnsRecordType Type => this._record.Type; + + public DnsRecordClass Class => this._record.Class; + + public TimeSpan TimeToLive => this._record.TimeToLive; + + public Int32 DataLength => this._record.DataLength; + + public Byte[] Data => this._record.Data; + + public Int32 Size => this._record.Size; + + protected virtual String[] IncludedProperties + => new[] { nameof(this.Name), nameof(this.Type), nameof(this.Class), nameof(this.TimeToLive), nameof(this.DataLength) }; + + public Byte[] ToArray() => this._record.ToArray(); + + public override String ToString() + => Json.SerializeOnly(this, true, this.IncludedProperties); + } + + public class DnsResourceRecord : IDnsResourceRecord { + public DnsResourceRecord( + DnsDomain domain, + Byte[] data, + DnsRecordType type, + DnsRecordClass klass = DnsRecordClass.IN, + TimeSpan ttl = default) { + this.Name = domain; + this.Type = type; + this.Class = klass; + this.TimeToLive = ttl; + this.Data = data; + } + + public DnsDomain Name { + get; + } + + public DnsRecordType Type { + get; + } + + public DnsRecordClass Class { + get; + } + + public TimeSpan TimeToLive { + get; + } + + public Int32 DataLength => this.Data.Length; + + public Byte[] Data { + get; + } + + public Int32 Size => this.Name.Size + Tail.SIZE + this.Data.Length; + + public static IList GetAllFromArray( + Byte[] message, + Int32 offset, + Int32 count, + out Int32 endOffset) { + IList records = new List(count); + + for(Int32 i = 0; i < count; i++) { + records.Add(FromArray(message, offset, out offset)); + } + + endOffset = offset; + return records; + } + + public static DnsResourceRecord FromArray(Byte[] message, Int32 offset, out Int32 endOffset) { + DnsDomain domain = DnsDomain.FromArray(message, offset, out offset); + Tail tail = message.ToStruct(offset, Tail.SIZE); + + Byte[] data = new Byte[tail.DataLength]; + + offset += Tail.SIZE; + Array.Copy(message, offset, data, 0, data.Length); + + endOffset = offset + data.Length; + + return new DnsResourceRecord(domain, data, tail.Type, tail.Class, tail.TimeToLive); + } + + public Byte[] ToArray() => new MemoryStream(this.Size) + .Append(this.Name.ToArray()) + .Append(new Tail() { + Type = Type, + Class = Class, + TimeToLive = TimeToLive, + DataLength = this.Data.Length, + }.ToBytes()) + .Append(this.Data) + .ToArray(); + + public override String ToString() => Json.SerializeOnly( + this, + true, + nameof(this.Name), + nameof(this.Type), + nameof(this.Class), + nameof(this.TimeToLive), + nameof(this.DataLength)); + + [StructEndianness(Endianness.Big)] + [StructLayout(LayoutKind.Sequential, Pack = 2)] + private struct Tail { + public const Int32 SIZE = 10; + + private UInt16 type; + private UInt16 klass; + private UInt32 ttl; + private UInt16 dataLength; + + public DnsRecordType Type { + get => (DnsRecordType)this.type; + set => this.type = (UInt16)value; + } + + public DnsRecordClass Class { + get => (DnsRecordClass)this.klass; + set => this.klass = (UInt16)value; + } + + public TimeSpan TimeToLive { + get => TimeSpan.FromSeconds(this.ttl); + set => this.ttl = (UInt32)value.TotalSeconds; + } + + public Int32 DataLength { + get => this.dataLength; + set => this.dataLength = (UInt16)value; + } + } + } + + public class DnsPointerResourceRecord : DnsResourceRecordBase { + public DnsPointerResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) + : base(record) => this.PointerDomainName = DnsDomain.FromArray(message, dataOffset); + + public DnsDomain PointerDomainName { + get; + } + + protected override String[] IncludedProperties { + get { + List temp = new List(base.IncludedProperties) { nameof(this.PointerDomainName) }; + return temp.ToArray(); + } + } + } + + public class DnsIPAddressResourceRecord : DnsResourceRecordBase { + public DnsIPAddressResourceRecord(IDnsResourceRecord record) + : base(record) => this.IPAddress = new IPAddress(this.Data); + + public IPAddress IPAddress { + get; + } + + protected override String[] IncludedProperties { + get { + List temp = new List(base.IncludedProperties) { nameof(this.IPAddress) }; + return temp.ToArray(); + } + } + } + + public class DnsNameServerResourceRecord : DnsResourceRecordBase { + public DnsNameServerResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) + : base(record) => this.NSDomainName = DnsDomain.FromArray(message, dataOffset); + + public DnsDomain NSDomainName { + get; + } + + protected override String[] IncludedProperties { + get { + List temp = new List(base.IncludedProperties) { nameof(this.NSDomainName) }; + return temp.ToArray(); + } + } + } + + public class DnsCanonicalNameResourceRecord : DnsResourceRecordBase { + public DnsCanonicalNameResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) + : base(record) => this.CanonicalDomainName = DnsDomain.FromArray(message, dataOffset); + + public DnsDomain CanonicalDomainName { + get; + } + + protected override String[] IncludedProperties => new List(base.IncludedProperties) + { + nameof(this.CanonicalDomainName), + }.ToArray(); + } + + public class DnsMailExchangeResourceRecord : DnsResourceRecordBase { + private const Int32 PreferenceSize = 2; + + public DnsMailExchangeResourceRecord( + IDnsResourceRecord record, + Byte[] message, + Int32 dataOffset) + : base(record) { + Byte[] preference = new Byte[PreferenceSize]; + Array.Copy(message, dataOffset, preference, 0, preference.Length); + + if(BitConverter.IsLittleEndian) { + Array.Reverse(preference); + } + + dataOffset += PreferenceSize; + + this.Preference = BitConverter.ToUInt16(preference, 0); + this.ExchangeDomainName = DnsDomain.FromArray(message, dataOffset); + } + + public Int32 Preference { + get; + } + + public DnsDomain ExchangeDomainName { + get; + } + + protected override String[] IncludedProperties => new List(base.IncludedProperties) + { + nameof(this.Preference), + nameof(this.ExchangeDomainName), + }.ToArray(); + } + + public class DnsStartOfAuthorityResourceRecord : DnsResourceRecordBase { + public DnsStartOfAuthorityResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) + : base(record) { + this.MasterDomainName = DnsDomain.FromArray(message, dataOffset, out dataOffset); + this.ResponsibleDomainName = DnsDomain.FromArray(message, dataOffset, out dataOffset); + + Options tail = message.ToStruct(dataOffset, Options.SIZE); + + this.SerialNumber = tail.SerialNumber; + this.RefreshInterval = tail.RefreshInterval; + this.RetryInterval = tail.RetryInterval; + this.ExpireInterval = tail.ExpireInterval; + this.MinimumTimeToLive = tail.MinimumTimeToLive; + } + + public DnsStartOfAuthorityResourceRecord( + DnsDomain domain, + DnsDomain master, + DnsDomain responsible, + Int64 serial, + TimeSpan refresh, + TimeSpan retry, + TimeSpan expire, + TimeSpan minTtl, + TimeSpan ttl = default) + : base(Create(domain, master, responsible, serial, refresh, retry, expire, minTtl, ttl)) { + this.MasterDomainName = master; + this.ResponsibleDomainName = responsible; + + this.SerialNumber = serial; + this.RefreshInterval = refresh; + this.RetryInterval = retry; + this.ExpireInterval = expire; + this.MinimumTimeToLive = minTtl; + } + + public DnsDomain MasterDomainName { + get; + } + + public DnsDomain ResponsibleDomainName { + get; + } + + public Int64 SerialNumber { + get; + } + + public TimeSpan RefreshInterval { + get; + } + + public TimeSpan RetryInterval { + get; + } + + public TimeSpan ExpireInterval { + get; + } + + public TimeSpan MinimumTimeToLive { + get; + } + + protected override String[] IncludedProperties => new List(base.IncludedProperties) + { + nameof(this.MasterDomainName), + nameof(this.ResponsibleDomainName), + nameof(this.SerialNumber), + }.ToArray(); + + private static IDnsResourceRecord Create( + DnsDomain domain, + DnsDomain master, + DnsDomain responsible, + Int64 serial, + TimeSpan refresh, + TimeSpan retry, + TimeSpan expire, + TimeSpan minTtl, + TimeSpan ttl) { + MemoryStream data = new MemoryStream(Options.SIZE + master.Size + responsible.Size); + Options tail = new Options { + SerialNumber = serial, + RefreshInterval = refresh, + RetryInterval = retry, + ExpireInterval = expire, + MinimumTimeToLive = minTtl, + }; + + _ = data.Append(master.ToArray()).Append(responsible.ToArray()).Append(tail.ToBytes()); + + return new DnsResourceRecord(domain, data.ToArray(), DnsRecordType.SOA, DnsRecordClass.IN, ttl); + } + + [StructEndianness(Endianness.Big)] + [StructLayout(LayoutKind.Sequential, Pack = 4)] + public struct Options { + public const Int32 SIZE = 20; + + private UInt32 serialNumber; + private UInt32 refreshInterval; + private UInt32 retryInterval; + private UInt32 expireInterval; + private UInt32 ttl; + + public Int64 SerialNumber { + get => this.serialNumber; + set => this.serialNumber = (UInt32)value; + } + + public TimeSpan RefreshInterval { + get => TimeSpan.FromSeconds(this.refreshInterval); + set => this.refreshInterval = (UInt32)value.TotalSeconds; + } + + public TimeSpan RetryInterval { + get => TimeSpan.FromSeconds(this.retryInterval); + set => this.retryInterval = (UInt32)value.TotalSeconds; + } + + public TimeSpan ExpireInterval { + get => TimeSpan.FromSeconds(this.expireInterval); + set => this.expireInterval = (UInt32)value.TotalSeconds; + } + + public TimeSpan MinimumTimeToLive { + get => TimeSpan.FromSeconds(this.ttl); + set => this.ttl = (UInt32)value.TotalSeconds; + } + } + } + + private static class DnsResourceRecordFactory { + public static IList GetAllFromArray( + Byte[] message, + Int32 offset, + Int32 count, + out Int32 endOffset) { + List result = new List(count); + + for(Int32 i = 0; i < count; i++) { + result.Add(GetFromArray(message, offset, out offset)); + } + + endOffset = offset; + return result; + } + + private static IDnsResourceRecord GetFromArray(Byte[] message, Int32 offset, out Int32 endOffset) { + DnsResourceRecord record = DnsResourceRecord.FromArray(message, offset, out endOffset); + Int32 dataOffset = endOffset - record.DataLength; + + switch(record.Type) { + case DnsRecordType.A: + case DnsRecordType.AAAA: + return new DnsIPAddressResourceRecord(record); + case DnsRecordType.NS: + return new DnsNameServerResourceRecord(record, message, dataOffset); + case DnsRecordType.CNAME: + return new DnsCanonicalNameResourceRecord(record, message, dataOffset); + case DnsRecordType.SOA: + return new DnsStartOfAuthorityResourceRecord(record, message, dataOffset); + case DnsRecordType.PTR: + return new DnsPointerResourceRecord(record, message, dataOffset); + case DnsRecordType.MX: + return new DnsMailExchangeResourceRecord(record, message, dataOffset); + default: + return record; + } + } + } + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/DnsClient.Response.cs b/Unosquare.Swan/Networking/DnsClient.Response.cs index 8be8f9b..41e9d2e 100644 --- a/Unosquare.Swan/Networking/DnsClient.Response.cs +++ b/Unosquare.Swan/Networking/DnsClient.Response.cs @@ -1,215 +1,205 @@ -namespace Unosquare.Swan.Networking -{ - using Formatters; - using System; - using System.Collections.Generic; - using System.Collections.ObjectModel; - using System.IO; - using System.Linq; - - /// - /// DnsClient Response inner class. - /// - internal partial class DnsClient - { - public class DnsClientResponse : IDnsResponse - { - private readonly DnsResponse _response; - private readonly byte[] _message; - - internal DnsClientResponse(DnsClientRequest request, DnsResponse response, byte[] message) - { - Request = request; - - _message = message; - _response = response; - } - - public DnsClientRequest Request { get; } - - public int Id - { - get { return _response.Id; } - set { } - } - - public IList AnswerRecords => _response.AnswerRecords; - - public IList AuthorityRecords => - new ReadOnlyCollection(_response.AuthorityRecords); - - public IList AdditionalRecords => - new ReadOnlyCollection(_response.AdditionalRecords); - - public bool IsRecursionAvailable - { - get { return _response.IsRecursionAvailable; } - set { } - } - - public bool IsAuthorativeServer - { - get { return _response.IsAuthorativeServer; } - set { } - } - - public bool IsTruncated - { - get { return _response.IsTruncated; } - set { } - } - - public DnsOperationCode OperationCode - { - get { return _response.OperationCode; } - set { } - } - - public DnsResponseCode ResponseCode - { - get { return _response.ResponseCode; } - set { } - } - - public IList Questions => new ReadOnlyCollection(_response.Questions); - - public int Size => _message.Length; - - public byte[] ToArray() => _message; - - public override string ToString() => _response.ToString(); - } - - public class DnsResponse : IDnsResponse - { - private DnsHeader _header; - - public DnsResponse( - DnsHeader header, - IList questions, - IList answers, - IList authority, - IList additional) - { - _header = header; - Questions = questions; - AnswerRecords = answers; - AuthorityRecords = authority; - AdditionalRecords = additional; - } - - public IList Questions { get; } - - public IList AnswerRecords { get; } - - public IList AuthorityRecords { get; } - - public IList AdditionalRecords { get; } - - public int Id - { - get => _header.Id; - set => _header.Id = value; - } - - public bool IsRecursionAvailable - { - get => _header.RecursionAvailable; - set => _header.RecursionAvailable = value; - } - - public bool IsAuthorativeServer - { - get => _header.AuthorativeServer; - set => _header.AuthorativeServer = value; - } - - public bool IsTruncated - { - get => _header.Truncated; - set => _header.Truncated = value; - } - - public DnsOperationCode OperationCode - { - get => _header.OperationCode; - set => _header.OperationCode = value; - } - - public DnsResponseCode ResponseCode - { - get => _header.ResponseCode; - set => _header.ResponseCode = value; - } - - public int Size - => _header.Size + - Questions.Sum(q => q.Size) + - AnswerRecords.Sum(a => a.Size) + - AuthorityRecords.Sum(a => a.Size) + - AdditionalRecords.Sum(a => a.Size); - - public static DnsResponse FromArray(byte[] message) - { - var header = DnsHeader.FromArray(message); - var offset = header.Size; - - if (!header.Response || header.QuestionCount == 0) - { - throw new ArgumentException("Invalid response message"); - } - - if (header.Truncated) - { - return new DnsResponse(header, - DnsQuestion.GetAllFromArray(message, offset, header.QuestionCount), - new List(), - new List(), - new List()); - } - - return new DnsResponse(header, - DnsQuestion.GetAllFromArray(message, offset, header.QuestionCount, out offset), - DnsResourceRecordFactory.GetAllFromArray(message, offset, header.AnswerRecordCount, out offset), - DnsResourceRecordFactory.GetAllFromArray(message, offset, header.AuthorityRecordCount, out offset), - DnsResourceRecordFactory.GetAllFromArray(message, offset, header.AdditionalRecordCount, out offset)); - } - - public byte[] ToArray() - { - UpdateHeader(); - var result = new MemoryStream(Size); - - result - .Append(_header.ToArray()) - .Append(Questions.Select(q => q.ToArray())) - .Append(AnswerRecords.Select(a => a.ToArray())) - .Append(AuthorityRecords.Select(a => a.ToArray())) - .Append(AdditionalRecords.Select(a => a.ToArray())); - - return result.ToArray(); - } - - public override string ToString() - { - UpdateHeader(); - - return Json.SerializeOnly( - this, - true, - nameof(Questions), - nameof(AnswerRecords), - nameof(AuthorityRecords), - nameof(AdditionalRecords)); - } - - private void UpdateHeader() - { - _header.QuestionCount = Questions.Count; - _header.AnswerRecordCount = AnswerRecords.Count; - _header.AuthorityRecordCount = AuthorityRecords.Count; - _header.AdditionalRecordCount = AdditionalRecords.Count; - } - } - } +using Unosquare.Swan.Formatters; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; + +namespace Unosquare.Swan.Networking { + /// + /// DnsClient Response inner class. + /// + internal partial class DnsClient { + public class DnsClientResponse : IDnsResponse { + private readonly DnsResponse _response; + private readonly Byte[] _message; + + internal DnsClientResponse(DnsClientRequest request, DnsResponse response, Byte[] message) { + this.Request = request; + + this._message = message; + this._response = response; + } + + public DnsClientRequest Request { + get; + } + + public Int32 Id { + get => this._response.Id; + set { + } + } + + public IList AnswerRecords => this._response.AnswerRecords; + + public IList AuthorityRecords => + new ReadOnlyCollection(this._response.AuthorityRecords); + + public IList AdditionalRecords => + new ReadOnlyCollection(this._response.AdditionalRecords); + + public Boolean IsRecursionAvailable { + get => this._response.IsRecursionAvailable; + set { + } + } + + public Boolean IsAuthorativeServer { + get => this._response.IsAuthorativeServer; + set { + } + } + + public Boolean IsTruncated { + get => this._response.IsTruncated; + set { + } + } + + public DnsOperationCode OperationCode { + get => this._response.OperationCode; + set { + } + } + + public DnsResponseCode ResponseCode { + get => this._response.ResponseCode; + set { + } + } + + public IList Questions => new ReadOnlyCollection(this._response.Questions); + + public Int32 Size => this._message.Length; + + public Byte[] ToArray() => this._message; + + public override String ToString() => this._response.ToString(); + } + + public class DnsResponse : IDnsResponse { + private DnsHeader _header; + + public DnsResponse( + DnsHeader header, + IList questions, + IList answers, + IList authority, + IList additional) { + this._header = header; + this.Questions = questions; + this.AnswerRecords = answers; + this.AuthorityRecords = authority; + this.AdditionalRecords = additional; + } + + public IList Questions { + get; + } + + public IList AnswerRecords { + get; + } + + public IList AuthorityRecords { + get; + } + + public IList AdditionalRecords { + get; + } + + public Int32 Id { + get => this._header.Id; + set => this._header.Id = value; + } + + public Boolean IsRecursionAvailable { + get => this._header.RecursionAvailable; + set => this._header.RecursionAvailable = value; + } + + public Boolean IsAuthorativeServer { + get => this._header.AuthorativeServer; + set => this._header.AuthorativeServer = value; + } + + public Boolean IsTruncated { + get => this._header.Truncated; + set => this._header.Truncated = value; + } + + public DnsOperationCode OperationCode { + get => this._header.OperationCode; + set => this._header.OperationCode = value; + } + + public DnsResponseCode ResponseCode { + get => this._header.ResponseCode; + set => this._header.ResponseCode = value; + } + + public Int32 Size + => this._header.Size + + this.Questions.Sum(q => q.Size) + + this.AnswerRecords.Sum(a => a.Size) + + this.AuthorityRecords.Sum(a => a.Size) + + this.AdditionalRecords.Sum(a => a.Size); + + public static DnsResponse FromArray(Byte[] message) { + DnsHeader header = DnsHeader.FromArray(message); + Int32 offset = header.Size; + + if(!header.Response || header.QuestionCount == 0) { + throw new ArgumentException("Invalid response message"); + } + + return header.Truncated + ? new DnsResponse(header, + DnsQuestion.GetAllFromArray(message, offset, header.QuestionCount), + new List(), + new List(), + new List()) + : new DnsResponse(header, + DnsQuestion.GetAllFromArray(message, offset, header.QuestionCount, out offset), + DnsResourceRecordFactory.GetAllFromArray(message, offset, header.AnswerRecordCount, out offset), + DnsResourceRecordFactory.GetAllFromArray(message, offset, header.AuthorityRecordCount, out offset), + DnsResourceRecordFactory.GetAllFromArray(message, offset, header.AdditionalRecordCount, out _)); + } + + public Byte[] ToArray() { + this.UpdateHeader(); + MemoryStream result = new MemoryStream(this.Size); + + _ = result + .Append(this._header.ToArray()) + .Append(this.Questions.Select(q => q.ToArray())) + .Append(this.AnswerRecords.Select(a => a.ToArray())) + .Append(this.AuthorityRecords.Select(a => a.ToArray())) + .Append(this.AdditionalRecords.Select(a => a.ToArray())); + + return result.ToArray(); + } + + public override String ToString() { + this.UpdateHeader(); + + return Json.SerializeOnly( + this, + true, + nameof(this.Questions), + nameof(this.AnswerRecords), + nameof(this.AuthorityRecords), + nameof(this.AdditionalRecords)); + } + + private void UpdateHeader() { + this._header.QuestionCount = this.Questions.Count; + this._header.AnswerRecordCount = this.AnswerRecords.Count; + this._header.AuthorityRecordCount = this.AuthorityRecords.Count; + this._header.AdditionalRecordCount = this.AdditionalRecords.Count; + } + } + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/DnsClient.cs b/Unosquare.Swan/Networking/DnsClient.cs index 70abcd9..9ff7f04 100644 --- a/Unosquare.Swan/Networking/DnsClient.cs +++ b/Unosquare.Swan/Networking/DnsClient.cs @@ -1,88 +1,77 @@ -namespace Unosquare.Swan.Networking -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Net; - using Exceptions; - - /// - /// DnsClient public methods. - /// - internal partial class DnsClient - { - private readonly IPEndPoint _dns; - private readonly IDnsRequestResolver _resolver; - - public DnsClient(IPEndPoint dns, IDnsRequestResolver resolver = null) - { - _dns = dns; - _resolver = resolver ?? new DnsUdpRequestResolver(new DnsTcpRequestResolver()); - } - - public DnsClient(IPAddress ip, int port = Network.DnsDefaultPort, IDnsRequestResolver resolver = null) - : this(new IPEndPoint(ip, port), resolver) - { - } - - public DnsClientRequest Create(IDnsRequest request = null) - { - return new DnsClientRequest(_dns, request, _resolver); - } - - public IList Lookup(string domain, DnsRecordType type = DnsRecordType.A) - { - if (string.IsNullOrWhiteSpace(domain)) - throw new ArgumentNullException(nameof(domain)); - - if (type != DnsRecordType.A && type != DnsRecordType.AAAA) - { - throw new ArgumentException("Invalid record type " + type); - } - - var response = Resolve(domain, type); - var ips = response.AnswerRecords - .Where(r => r.Type == type) - .Cast() - .Select(r => r.IPAddress) - .ToList(); - - if (ips.Count == 0) - { - throw new DnsQueryException(response, "No matching records"); - } - - return ips; - } - - public string Reverse(IPAddress ip) - { - if (ip == null) - throw new ArgumentNullException(nameof(ip)); - - var response = Resolve(DnsDomain.PointerName(ip), DnsRecordType.PTR); - var ptr = response.AnswerRecords.FirstOrDefault(r => r.Type == DnsRecordType.PTR); - - if (ptr == null) - { - throw new DnsQueryException(response, "No matching records"); - } - - return ((DnsPointerResourceRecord)ptr).PointerDomainName.ToString(); - } - - public DnsClientResponse Resolve(string domain, DnsRecordType type) => Resolve(new DnsDomain(domain), type); - - public DnsClientResponse Resolve(DnsDomain domain, DnsRecordType type) - { - var request = Create(); - var question = new DnsQuestion(domain, type); - - request.Questions.Add(question); - request.OperationCode = DnsOperationCode.Query; - request.RecursionDesired = true; - - return request.Resolve(); - } - } +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using Unosquare.Swan.Exceptions; + +namespace Unosquare.Swan.Networking { + /// + /// DnsClient public methods. + /// + internal partial class DnsClient { + private readonly IPEndPoint _dns; + private readonly IDnsRequestResolver _resolver; + + public DnsClient(IPEndPoint dns, IDnsRequestResolver resolver = null) { + this._dns = dns; + this._resolver = resolver ?? new DnsUdpRequestResolver(new DnsTcpRequestResolver()); + } + + public DnsClient(IPAddress ip, Int32 port = Network.DnsDefaultPort, IDnsRequestResolver resolver = null) + : this(new IPEndPoint(ip, port), resolver) { + } + + public DnsClientRequest Create(IDnsRequest request = null) => new DnsClientRequest(this._dns, request, this._resolver); + + public IList Lookup(String domain, DnsRecordType type = DnsRecordType.A) { + if(String.IsNullOrWhiteSpace(domain)) { + throw new ArgumentNullException(nameof(domain)); + } + + if(type != DnsRecordType.A && type != DnsRecordType.AAAA) { + throw new ArgumentException("Invalid record type " + type); + } + + DnsClientResponse response = this.Resolve(domain, type); + List ips = response.AnswerRecords + .Where(r => r.Type == type) + .Cast() + .Select(r => r.IPAddress) + .ToList(); + + if(ips.Count == 0) { + throw new DnsQueryException(response, "No matching records"); + } + + return ips; + } + + public String Reverse(IPAddress ip) { + if(ip == null) { + throw new ArgumentNullException(nameof(ip)); + } + + DnsClientResponse response = this.Resolve(DnsDomain.PointerName(ip), DnsRecordType.PTR); + IDnsResourceRecord ptr = response.AnswerRecords.FirstOrDefault(r => r.Type == DnsRecordType.PTR); + + if(ptr == null) { + throw new DnsQueryException(response, "No matching records"); + } + + return ((DnsPointerResourceRecord)ptr).PointerDomainName.ToString(); + } + + public DnsClientResponse Resolve(String domain, DnsRecordType type) => this.Resolve(new DnsDomain(domain), type); + + public DnsClientResponse Resolve(DnsDomain domain, DnsRecordType type) { + DnsClientRequest request = this.Create(); + DnsQuestion question = new DnsQuestion(domain, type); + + request.Questions.Add(question); + request.OperationCode = DnsOperationCode.Query; + request.RecursionDesired = true; + + return request.Resolve(); + } + } } diff --git a/Unosquare.Swan/Networking/DnsQueryResult.cs b/Unosquare.Swan/Networking/DnsQueryResult.cs index fdc4823..c348f7d 100644 --- a/Unosquare.Swan/Networking/DnsQueryResult.cs +++ b/Unosquare.Swan/Networking/DnsQueryResult.cs @@ -1,123 +1,132 @@ -namespace Unosquare.Swan.Networking -{ - using System.Collections.Generic; - +using System; +using System.Collections.Generic; + +namespace Unosquare.Swan.Networking { + /// + /// Represents a response from a DNS server. + /// + public class DnsQueryResult { + private readonly List m_AnswerRecords = new List(); + private readonly List m_AdditionalRecords = new List(); + private readonly List m_AuthorityRecords = new List(); + /// - /// Represents a response from a DNS server. + /// Initializes a new instance of the class. /// - public class DnsQueryResult - { - private readonly List m_AnswerRecords = new List(); - private readonly List m_AdditionalRecords = new List(); - private readonly List m_AuthorityRecords = new List(); - - /// - /// Initializes a new instance of the class. - /// - /// The response. - internal DnsQueryResult(DnsClient.DnsClientResponse response) - : this() - { - Id = response.Id; - IsAuthoritativeServer = response.IsAuthorativeServer; - IsRecursionAvailable = response.IsRecursionAvailable; - IsTruncated = response.IsTruncated; - OperationCode = response.OperationCode; - ResponseCode = response.ResponseCode; - - if (response.AnswerRecords != null) - { - foreach (var record in response.AnswerRecords) - AnswerRecords.Add(new DnsRecord(record)); - } - - if (response.AuthorityRecords != null) - { - foreach (var record in response.AuthorityRecords) - AuthorityRecords.Add(new DnsRecord(record)); - } - - if (response.AdditionalRecords != null) - { - foreach (var record in response.AdditionalRecords) - AdditionalRecords.Add(new DnsRecord(record)); - } - } - - private DnsQueryResult() - { - } - - /// - /// Gets the identifier. - /// - /// - /// The identifier. - /// - public int Id { get; } - - /// - /// Gets a value indicating whether this instance is authoritative server. - /// - /// - /// true if this instance is authoritative server; otherwise, false. - /// - public bool IsAuthoritativeServer { get; } - - /// - /// Gets a value indicating whether this instance is truncated. - /// - /// - /// true if this instance is truncated; otherwise, false. - /// - public bool IsTruncated { get; } - - /// - /// Gets a value indicating whether this instance is recursion available. - /// - /// - /// true if this instance is recursion available; otherwise, false. - /// - public bool IsRecursionAvailable { get; } - - /// - /// Gets the operation code. - /// - /// - /// The operation code. - /// - public DnsOperationCode OperationCode { get; } - - /// - /// Gets the response code. - /// - /// - /// The response code. - /// - public DnsResponseCode ResponseCode { get; } - - /// - /// Gets the answer records. - /// - /// - /// The answer records. - /// - public IList AnswerRecords => m_AnswerRecords; - - /// - /// Gets the additional records. - /// - /// - /// The additional records. - /// - public IList AdditionalRecords => m_AdditionalRecords; - - /// - /// Gets the authority records. - /// - /// - /// The authority records. - /// - public IList AuthorityRecords => m_AuthorityRecords; - } + /// The response. + internal DnsQueryResult(DnsClient.DnsClientResponse response) + : this() { + this.Id = response.Id; + this.IsAuthoritativeServer = response.IsAuthorativeServer; + this.IsRecursionAvailable = response.IsRecursionAvailable; + this.IsTruncated = response.IsTruncated; + this.OperationCode = response.OperationCode; + this.ResponseCode = response.ResponseCode; + + if(response.AnswerRecords != null) { + foreach(DnsClient.IDnsResourceRecord record in response.AnswerRecords) { + this.AnswerRecords.Add(new DnsRecord(record)); + } + } + + if(response.AuthorityRecords != null) { + foreach(DnsClient.IDnsResourceRecord record in response.AuthorityRecords) { + this.AuthorityRecords.Add(new DnsRecord(record)); + } + } + + if(response.AdditionalRecords != null) { + foreach(DnsClient.IDnsResourceRecord record in response.AdditionalRecords) { + this.AdditionalRecords.Add(new DnsRecord(record)); + } + } + } + + private DnsQueryResult() { + } + + /// + /// Gets the identifier. + /// + /// + /// The identifier. + /// + public Int32 Id { + get; + } + + /// + /// Gets a value indicating whether this instance is authoritative server. + /// + /// + /// true if this instance is authoritative server; otherwise, false. + /// + public Boolean IsAuthoritativeServer { + get; + } + + /// + /// Gets a value indicating whether this instance is truncated. + /// + /// + /// true if this instance is truncated; otherwise, false. + /// + public Boolean IsTruncated { + get; + } + + /// + /// Gets a value indicating whether this instance is recursion available. + /// + /// + /// true if this instance is recursion available; otherwise, false. + /// + public Boolean IsRecursionAvailable { + get; + } + + /// + /// Gets the operation code. + /// + /// + /// The operation code. + /// + public DnsOperationCode OperationCode { + get; + } + + /// + /// Gets the response code. + /// + /// + /// The response code. + /// + public DnsResponseCode ResponseCode { + get; + } + + /// + /// Gets the answer records. + /// + /// + /// The answer records. + /// + public IList AnswerRecords => this.m_AnswerRecords; + + /// + /// Gets the additional records. + /// + /// + /// The additional records. + /// + public IList AdditionalRecords => this.m_AdditionalRecords; + + /// + /// Gets the authority records. + /// + /// + /// The authority records. + /// + public IList AuthorityRecords => this.m_AuthorityRecords; + } } diff --git a/Unosquare.Swan/Networking/DnsRecord.cs b/Unosquare.Swan/Networking/DnsRecord.cs index 272001e..33234e8 100644 --- a/Unosquare.Swan/Networking/DnsRecord.cs +++ b/Unosquare.Swan/Networking/DnsRecord.cs @@ -1,208 +1,240 @@ -namespace Unosquare.Swan.Networking -{ - using System; - using System.Net; - using System.Text; - +using System; +using System.Net; +using System.Text; + +namespace Unosquare.Swan.Networking { + /// + /// Represents a DNS record entry. + /// + public class DnsRecord { /// - /// Represents a DNS record entry. + /// Initializes a new instance of the class. /// - public class DnsRecord - { - /// - /// Initializes a new instance of the class. - /// - /// The record. - internal DnsRecord(DnsClient.IDnsResourceRecord record) - : this() - { - Name = record.Name.ToString(); - Type = record.Type; - Class = record.Class; - TimeToLive = record.TimeToLive; - Data = record.Data; - - // PTR - PointerDomainName = (record as DnsClient.DnsPointerResourceRecord)?.PointerDomainName?.ToString(); - - // A - IPAddress = (record as DnsClient.DnsIPAddressResourceRecord)?.IPAddress; - - // NS - NameServerDomainName = (record as DnsClient.DnsNameServerResourceRecord)?.NSDomainName?.ToString(); - - // CNAME - CanonicalDomainName = (record as DnsClient.DnsCanonicalNameResourceRecord)?.CanonicalDomainName.ToString(); - - // MX - MailExchangerDomainName = (record as DnsClient.DnsMailExchangeResourceRecord)?.ExchangeDomainName.ToString(); - MailExchangerPreference = (record as DnsClient.DnsMailExchangeResourceRecord)?.Preference; - - // SOA - SoaMasterDomainName = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.MasterDomainName.ToString(); - SoaResponsibleDomainName = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.ResponsibleDomainName.ToString(); - SoaSerialNumber = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.SerialNumber; - SoaRefreshInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.RefreshInterval; - SoaRetryInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.RetryInterval; - SoaExpireInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.ExpireInterval; - SoaMinimumTimeToLive = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.MinimumTimeToLive; - } - - private DnsRecord() - { - // placeholder - } - - /// - /// Gets the name. - /// - /// - /// The name. - /// - public string Name { get; } - - /// - /// Gets the type. - /// - /// - /// The type. - /// - public DnsRecordType Type { get; } - - /// - /// Gets the class. - /// - /// - /// The class. - /// - public DnsRecordClass Class { get; } - - /// - /// Gets the time to live. - /// - /// - /// The time to live. - /// - public TimeSpan TimeToLive { get; } - - /// - /// Gets the raw data of the record. - /// - /// - /// The data. - /// - public byte[] Data { get; } - - /// - /// Gets the data text bytes in ASCII encoding. - /// - /// - /// The data text. - /// - public string DataText => Data == null ? string.Empty : Encoding.ASCII.GetString(Data); - - /// - /// Gets the name of the pointer domain. - /// - /// - /// The name of the pointer domain. - /// - public string PointerDomainName { get; } - - /// - /// Gets the ip address. - /// - /// - /// The ip address. - /// - public IPAddress IPAddress { get; } - - /// - /// Gets the name of the name server domain. - /// - /// - /// The name of the name server domain. - /// - public string NameServerDomainName { get; } - - /// - /// Gets the name of the canonical domain. - /// - /// - /// The name of the canonical domain. - /// - public string CanonicalDomainName { get; } - - /// - /// Gets the mail exchanger preference. - /// - /// - /// The mail exchanger preference. - /// - public int? MailExchangerPreference { get; } - - /// - /// Gets the name of the mail exchanger domain. - /// - /// - /// The name of the mail exchanger domain. - /// - public string MailExchangerDomainName { get; } - - /// - /// Gets the name of the soa master domain. - /// - /// - /// The name of the soa master domain. - /// - public string SoaMasterDomainName { get; } - - /// - /// Gets the name of the soa responsible domain. - /// - /// - /// The name of the soa responsible domain. - /// - public string SoaResponsibleDomainName { get; } - - /// - /// Gets the soa serial number. - /// - /// - /// The soa serial number. - /// - public long? SoaSerialNumber { get; } - - /// - /// Gets the soa refresh interval. - /// - /// - /// The soa refresh interval. - /// - public TimeSpan? SoaRefreshInterval { get; } - - /// - /// Gets the soa retry interval. - /// - /// - /// The soa retry interval. - /// - public TimeSpan? SoaRetryInterval { get; } - - /// - /// Gets the soa expire interval. - /// - /// - /// The soa expire interval. - /// - public TimeSpan? SoaExpireInterval { get; } - - /// - /// Gets the soa minimum time to live. - /// - /// - /// The soa minimum time to live. - /// - public TimeSpan? SoaMinimumTimeToLive { get; } - } + /// The record. + internal DnsRecord(DnsClient.IDnsResourceRecord record) + : this() { + this.Name = record.Name.ToString(); + this.Type = record.Type; + this.Class = record.Class; + this.TimeToLive = record.TimeToLive; + this.Data = record.Data; + + // PTR + this.PointerDomainName = (record as DnsClient.DnsPointerResourceRecord)?.PointerDomainName?.ToString(); + + // A + this.IPAddress = (record as DnsClient.DnsIPAddressResourceRecord)?.IPAddress; + + // NS + this.NameServerDomainName = (record as DnsClient.DnsNameServerResourceRecord)?.NSDomainName?.ToString(); + + // CNAME + this.CanonicalDomainName = (record as DnsClient.DnsCanonicalNameResourceRecord)?.CanonicalDomainName.ToString(); + + // MX + this.MailExchangerDomainName = (record as DnsClient.DnsMailExchangeResourceRecord)?.ExchangeDomainName.ToString(); + this.MailExchangerPreference = (record as DnsClient.DnsMailExchangeResourceRecord)?.Preference; + + // SOA + this.SoaMasterDomainName = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.MasterDomainName.ToString(); + this.SoaResponsibleDomainName = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.ResponsibleDomainName.ToString(); + this.SoaSerialNumber = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.SerialNumber; + this.SoaRefreshInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.RefreshInterval; + this.SoaRetryInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.RetryInterval; + this.SoaExpireInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.ExpireInterval; + this.SoaMinimumTimeToLive = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.MinimumTimeToLive; + } + + private DnsRecord() { + // placeholder + } + + /// + /// Gets the name. + /// + /// + /// The name. + /// + public String Name { + get; + } + + /// + /// Gets the type. + /// + /// + /// The type. + /// + public DnsRecordType Type { + get; + } + + /// + /// Gets the class. + /// + /// + /// The class. + /// + public DnsRecordClass Class { + get; + } + + /// + /// Gets the time to live. + /// + /// + /// The time to live. + /// + public TimeSpan TimeToLive { + get; + } + + /// + /// Gets the raw data of the record. + /// + /// + /// The data. + /// + public Byte[] Data { + get; + } + + /// + /// Gets the data text bytes in ASCII encoding. + /// + /// + /// The data text. + /// + public String DataText => this.Data == null ? String.Empty : Encoding.ASCII.GetString(this.Data); + + /// + /// Gets the name of the pointer domain. + /// + /// + /// The name of the pointer domain. + /// + public String PointerDomainName { + get; + } + + /// + /// Gets the ip address. + /// + /// + /// The ip address. + /// + public IPAddress IPAddress { + get; + } + + /// + /// Gets the name of the name server domain. + /// + /// + /// The name of the name server domain. + /// + public String NameServerDomainName { + get; + } + + /// + /// Gets the name of the canonical domain. + /// + /// + /// The name of the canonical domain. + /// + public String CanonicalDomainName { + get; + } + + /// + /// Gets the mail exchanger preference. + /// + /// + /// The mail exchanger preference. + /// + public Int32? MailExchangerPreference { + get; + } + + /// + /// Gets the name of the mail exchanger domain. + /// + /// + /// The name of the mail exchanger domain. + /// + public String MailExchangerDomainName { + get; + } + + /// + /// Gets the name of the soa master domain. + /// + /// + /// The name of the soa master domain. + /// + public String SoaMasterDomainName { + get; + } + + /// + /// Gets the name of the soa responsible domain. + /// + /// + /// The name of the soa responsible domain. + /// + public String SoaResponsibleDomainName { + get; + } + + /// + /// Gets the soa serial number. + /// + /// + /// The soa serial number. + /// + public Int64? SoaSerialNumber { + get; + } + + /// + /// Gets the soa refresh interval. + /// + /// + /// The soa refresh interval. + /// + public TimeSpan? SoaRefreshInterval { + get; + } + + /// + /// Gets the soa retry interval. + /// + /// + /// The soa retry interval. + /// + public TimeSpan? SoaRetryInterval { + get; + } + + /// + /// Gets the soa expire interval. + /// + /// + /// The soa expire interval. + /// + public TimeSpan? SoaExpireInterval { + get; + } + + /// + /// Gets the soa minimum time to live. + /// + /// + /// The soa minimum time to live. + /// + public TimeSpan? SoaMinimumTimeToLive { + get; + } + } } diff --git a/Unosquare.Swan/Networking/Enums.Dns.cs b/Unosquare.Swan/Networking/Enums.Dns.cs index 7e2fe6b..75cbbf8 100644 --- a/Unosquare.Swan/Networking/Enums.Dns.cs +++ b/Unosquare.Swan/Networking/Enums.Dns.cs @@ -1,177 +1,172 @@ // ReSharper disable InconsistentNaming -namespace Unosquare.Swan.Networking -{ - #region DNS - +namespace Unosquare.Swan.Networking { + #region DNS + + /// + /// Enumerates the different DNS record types. + /// + public enum DnsRecordType { /// - /// Enumerates the different DNS record types. - /// - public enum DnsRecordType - { - /// - /// A records - /// - A = 1, - - /// - /// Nameserver records - /// - NS = 2, - - /// - /// CNAME records - /// - CNAME = 5, - - /// - /// SOA records - /// - SOA = 6, - - /// - /// WKS records - /// - WKS = 11, - - /// - /// PTR records - /// - PTR = 12, - - /// - /// MX records - /// - MX = 15, - - /// - /// TXT records - /// - TXT = 16, - - /// - /// A records fot IPv6 - /// - AAAA = 28, - - /// - /// SRV records - /// - SRV = 33, - - /// - /// ANY records - /// - ANY = 255, - } - + /// A records + /// + A = 1, + /// - /// Enumerates the different DNS record classes. - /// - public enum DnsRecordClass - { - /// - /// IN records - /// - IN = 1, - - /// - /// ANY records - /// - ANY = 255, - } - + /// Nameserver records + /// + NS = 2, + /// - /// Enumerates the different DNS operation codes. - /// - public enum DnsOperationCode - { - /// - /// Query operation - /// - Query = 0, - - /// - /// IQuery operation - /// - IQuery, - - /// - /// Status operation - /// - Status, - - /// - /// Notify operation - /// - Notify = 4, - - /// - /// Update operation - /// - Update, - } - + /// CNAME records + /// + CNAME = 5, + /// - /// Enumerates the different DNS query response codes. - /// - public enum DnsResponseCode - { - /// - /// No error - /// - NoError = 0, - - /// - /// No error - /// - FormatError, - - /// - /// Format error - /// - ServerFailure, - - /// - /// Server failure error - /// - NameError, - - /// - /// Name error - /// - NotImplemented, - - /// - /// Not implemented error - /// - Refused, - - /// - /// Refused error - /// - YXDomain, - - /// - /// YXRR error - /// - YXRRSet, - - /// - /// NXRR Set error - /// - NXRRSet, - - /// - /// Not authorized error - /// - NotAuth, - - /// - /// Not zone error - /// - NotZone, - } - - #endregion - + /// SOA records + /// + SOA = 6, + + /// + /// WKS records + /// + WKS = 11, + + /// + /// PTR records + /// + PTR = 12, + + /// + /// MX records + /// + MX = 15, + + /// + /// TXT records + /// + TXT = 16, + + /// + /// A records fot IPv6 + /// + AAAA = 28, + + /// + /// SRV records + /// + SRV = 33, + + /// + /// ANY records + /// + ANY = 255, + } + + /// + /// Enumerates the different DNS record classes. + /// + public enum DnsRecordClass { + /// + /// IN records + /// + IN = 1, + + /// + /// ANY records + /// + ANY = 255, + } + + /// + /// Enumerates the different DNS operation codes. + /// + public enum DnsOperationCode { + /// + /// Query operation + /// + Query = 0, + + /// + /// IQuery operation + /// + IQuery, + + /// + /// Status operation + /// + Status, + + /// + /// Notify operation + /// + Notify = 4, + + /// + /// Update operation + /// + Update, + } + + /// + /// Enumerates the different DNS query response codes. + /// + public enum DnsResponseCode { + /// + /// No error + /// + NoError = 0, + + /// + /// No error + /// + FormatError, + + /// + /// Format error + /// + ServerFailure, + + /// + /// Server failure error + /// + NameError, + + /// + /// Name error + /// + NotImplemented, + + /// + /// Not implemented error + /// + Refused, + + /// + /// Refused error + /// + YXDomain, + + /// + /// YXRR error + /// + YXRRSet, + + /// + /// NXRR Set error + /// + NXRRSet, + + /// + /// Not authorized error + /// + NotAuth, + + /// + /// Not zone error + /// + NotZone, + } + + #endregion + } diff --git a/Unosquare.Swan/Networking/Enums.Smtp.cs b/Unosquare.Swan/Networking/Enums.Smtp.cs index 5d05d02..b248585 100644 --- a/Unosquare.Swan/Networking/Enums.Smtp.cs +++ b/Unosquare.Swan/Networking/Enums.Smtp.cs @@ -1,6 +1,5 @@ // ReSharper disable InconsistentNaming -namespace Unosquare.Swan.Networking -{ +namespace Unosquare.Swan.Networking { #if NETSTANDARD1_3 /// @@ -134,167 +133,164 @@ namespace Unosquare.Swan.Networking GeneralFailure = -1, } #endif - + + /// + /// Enumerates all of the well-known SMTP command names. + /// + public enum SmtpCommandNames { /// - /// Enumerates all of the well-known SMTP command names. + /// An unknown command /// - public enum SmtpCommandNames - { - /// - /// An unknown command - /// - Unknown, - - /// - /// The helo command - /// - HELO, - - /// - /// The ehlo command - /// - EHLO, - - /// - /// The quit command - /// - QUIT, - - /// - /// The help command - /// - HELP, - - /// - /// The noop command - /// - NOOP, - - /// - /// The rset command - /// - RSET, - - /// - /// The mail command - /// - MAIL, - - /// - /// The data command - /// - DATA, - - /// - /// The send command - /// - SEND, - - /// - /// The soml command - /// - SOML, - - /// - /// The saml command - /// - SAML, - - /// - /// The RCPT command - /// - RCPT, - - /// - /// The vrfy command - /// - VRFY, - - /// - /// The expn command - /// - EXPN, - - /// - /// The starttls command - /// - STARTTLS, - - /// - /// The authentication command - /// - AUTH, - } - + Unknown, + /// - /// Enumerates the reply code severities. + /// The helo command /// - public enum SmtpReplyCodeSeverities - { - /// - /// The unknown severity - /// - Unknown = 0, - - /// - /// The positive completion severity - /// - PositiveCompletion = 200, - - /// - /// The positive intermediate severity - /// - PositiveIntermediate = 300, - - /// - /// The transient negative severity - /// - TransientNegative = 400, - - /// - /// The permanent negative severity - /// - PermanentNegative = 500, - } - + HELO, + /// - /// Enumerates the reply code categories. + /// The ehlo command /// - public enum SmtpReplyCodeCategories - { - /// - /// The unknown category - /// - Unknown = -1, - - /// - /// The syntax category - /// - Syntax = 0, - - /// - /// The information category - /// - Information = 1, - - /// - /// The connections category - /// - Connections = 2, - - /// - /// The unspecified a category - /// - UnspecifiedA = 3, - - /// - /// The unspecified b category - /// - UnspecifiedB = 4, - - /// - /// The system category - /// - System = 5, - } + EHLO, + + /// + /// The quit command + /// + QUIT, + + /// + /// The help command + /// + HELP, + + /// + /// The noop command + /// + NOOP, + + /// + /// The rset command + /// + RSET, + + /// + /// The mail command + /// + MAIL, + + /// + /// The data command + /// + DATA, + + /// + /// The send command + /// + SEND, + + /// + /// The soml command + /// + SOML, + + /// + /// The saml command + /// + SAML, + + /// + /// The RCPT command + /// + RCPT, + + /// + /// The vrfy command + /// + VRFY, + + /// + /// The expn command + /// + EXPN, + + /// + /// The starttls command + /// + STARTTLS, + + /// + /// The authentication command + /// + AUTH, + } + + /// + /// Enumerates the reply code severities. + /// + public enum SmtpReplyCodeSeverities { + /// + /// The unknown severity + /// + Unknown = 0, + + /// + /// The positive completion severity + /// + PositiveCompletion = 200, + + /// + /// The positive intermediate severity + /// + PositiveIntermediate = 300, + + /// + /// The transient negative severity + /// + TransientNegative = 400, + + /// + /// The permanent negative severity + /// + PermanentNegative = 500, + } + + /// + /// Enumerates the reply code categories. + /// + public enum SmtpReplyCodeCategories { + /// + /// The unknown category + /// + Unknown = -1, + + /// + /// The syntax category + /// + Syntax = 0, + + /// + /// The information category + /// + Information = 1, + + /// + /// The connections category + /// + Connections = 2, + + /// + /// The unspecified a category + /// + UnspecifiedA = 3, + + /// + /// The unspecified b category + /// + UnspecifiedB = 4, + + /// + /// The system category + /// + System = 5, + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/JsonClient.cs b/Unosquare.Swan/Networking/JsonClient.cs index 164a1fa..7a3fd2d 100644 --- a/Unosquare.Swan/Networking/JsonClient.cs +++ b/Unosquare.Swan/Networking/JsonClient.cs @@ -1,400 +1,378 @@ -namespace Unosquare.Swan.Networking -{ - using System; - using Exceptions; - using Models; - using Formatters; - using System.Collections.Generic; - using System.Net.Http; - using System.Security; - using System.Text; - using System.Threading; - using System.Threading.Tasks; - +using System; +using Unosquare.Swan.Exceptions; +using Unosquare.Swan.Models; +using Unosquare.Swan.Formatters; +using System.Collections.Generic; +using System.Net.Http; +using System.Security; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Unosquare.Swan.Networking { + /// + /// Represents a HttpClient with extended methods to use with JSON payloads + /// and bearer tokens authentication. + /// + public static class JsonClient { + private const String JsonMimeType = "application/json"; + /// - /// Represents a HttpClient with extended methods to use with JSON payloads - /// and bearer tokens authentication. + /// Post a object as JSON with optional authorization token. /// - public static class JsonClient - { - private const string JsonMimeType = "application/json"; - - /// - /// Post a object as JSON with optional authorization token. - /// - /// The type of response object. - /// The URL. - /// The payload. - /// The authorization. - /// The cancellation token. - /// A task with a result of the requested type. - public static async Task Post( - string url, - object payload, - string authorization = null, - CancellationToken ct = default) - { - var jsonString = await PostString(url, payload, authorization, ct).ConfigureAwait(false); - - return !string.IsNullOrEmpty(jsonString) ? Json.Deserialize(jsonString) : default; - } - - /// - /// Posts a object as JSON with optional authorization token and retrieve an object - /// or an error. - /// - /// The type of response object. - /// The type of the error. - /// The URL. - /// The payload. - /// The HTTP status error. - /// The authorization. - /// The cancellation token. - /// A task with a result of the requested type or an error object. - public static async Task> PostOrError( - string url, - object payload, - int httpStatusError = 500, - string authorization = null, - CancellationToken ct = default) - { - using (var httpClient = GetHttpClientWithAuthorizationHeader(authorization)) - { - var payloadJson = new StringContent(Json.Serialize(payload), Encoding.UTF8, JsonMimeType); - - var response = await httpClient.PostAsync(url, payloadJson, ct).ConfigureAwait(false); - - var jsonString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - - if (response.StatusCode == System.Net.HttpStatusCode.OK) - { - return OkOrError.FromOk(!string.IsNullOrEmpty(jsonString) + /// The type of response object. + /// The URL. + /// The payload. + /// The authorization. + /// The cancellation token. + /// A task with a result of the requested type. + public static async Task Post( + String url, + Object payload, + String authorization = null, + CancellationToken ct = default) { + String jsonString = await PostString(url, payload, authorization, ct).ConfigureAwait(false); + + return !String.IsNullOrEmpty(jsonString) ? Json.Deserialize(jsonString) : default; + } + + /// + /// Posts a object as JSON with optional authorization token and retrieve an object + /// or an error. + /// + /// The type of response object. + /// The type of the error. + /// The URL. + /// The payload. + /// The HTTP status error. + /// The authorization. + /// The cancellation token. + /// A task with a result of the requested type or an error object. + public static async Task> PostOrError( + String url, + Object payload, + Int32 httpStatusError = 500, + String authorization = null, + CancellationToken ct = default) { + using(HttpClient httpClient = GetHttpClientWithAuthorizationHeader(authorization)) { + StringContent payloadJson = new StringContent(Json.Serialize(payload), Encoding.UTF8, JsonMimeType); + + HttpResponseMessage response = await httpClient.PostAsync(url, payloadJson, ct).ConfigureAwait(false); + + String jsonString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + return response.StatusCode == System.Net.HttpStatusCode.OK + ? OkOrError.FromOk(!String.IsNullOrEmpty(jsonString) ? Json.Deserialize(jsonString) - : default); - } - - if ((int) response.StatusCode == httpStatusError) - { - return OkOrError.FromError(!string.IsNullOrEmpty(jsonString) + : default) + : (Int32)response.StatusCode == httpStatusError + ? OkOrError.FromError(!String.IsNullOrEmpty(jsonString) ? Json.Deserialize(jsonString) - : default); - } - - return new OkOrError(); - } - } - - /// - /// Posts the specified URL. - /// - /// The URL. - /// The payload. - /// The authorization. - /// The cancellation token. - /// A task with a result as a collection of key/value pairs. - public static async Task> Post( - string url, - object payload, - string authorization = null, - CancellationToken ct = default) - { - var jsonString = await PostString(url, payload, authorization, ct).ConfigureAwait(false); - - return string.IsNullOrWhiteSpace(jsonString) - ? default - : Json.Deserialize(jsonString) as IDictionary; - } - - /// - /// Posts the specified URL. - /// - /// The URL. - /// The payload. - /// The authorization. - /// The cancellation token. - /// - /// A task with a result of the requested string. - /// - /// url. - /// Error POST JSON. - public static Task PostString( - string url, - object payload, - string authorization = null, - CancellationToken ct = default) => SendAsync(HttpMethod.Post, url, payload, authorization, ct); - - /// - /// Puts the specified URL. - /// - /// The type of response object. - /// The URL. - /// The payload. - /// The authorization. - /// The cancellation token. - /// A task with a result of the requested type. - public static async Task Put( - string url, - object payload, - string authorization = null, - CancellationToken ct = default) - { - var jsonString = await PutString(url, payload, authorization, ct).ConfigureAwait(false); - - return !string.IsNullOrEmpty(jsonString) ? Json.Deserialize(jsonString) : default; - } - - /// - /// Puts the specified URL. - /// - /// The URL. - /// The payload. - /// The authorization. - /// The cancellation token. - /// A task with a result of the requested collection of key/value pairs. - public static async Task> Put( - string url, - object payload, - string authorization = null, - CancellationToken ct = default) - { - var response = await Put(url, payload, authorization, ct).ConfigureAwait(false); - - return response as IDictionary; - } - - /// - /// Puts as string. - /// - /// The URL. - /// The payload. - /// The authorization. - /// The cancellation token. - /// - /// A task with a result of the requested string. - /// - /// url. - /// Error PUT JSON. - public static Task PutString( - string url, - object payload, - string authorization = null, - CancellationToken ct = default) => SendAsync(HttpMethod.Put, url, payload, authorization, ct); - - /// - /// Gets as string. - /// - /// The URL. - /// The authorization. - /// The cancellation token. - /// - /// A task with a result of the requested string. - /// - /// url. - /// Error GET JSON. - public static async Task GetString( - string url, - string authorization = null, - CancellationToken ct = default) - { - var response = await GetHttpContent(url, authorization, ct).ConfigureAwait(false); - - return await response.ReadAsStringAsync().ConfigureAwait(false); - } - - /// - /// Gets the specified URL and return the JSON data as object - /// with optional authorization token. - /// - /// The response type. - /// The URL. - /// The authorization. - /// The cancellation token. - /// A task with a result of the requested type. - public static async Task Get( - string url, - string authorization = null, - CancellationToken ct = default) - { - var jsonString = await GetString(url, authorization, ct).ConfigureAwait(false); - - return !string.IsNullOrEmpty(jsonString) ? Json.Deserialize(jsonString) : default; - } - - /// - /// Gets the binary. - /// - /// The URL. - /// The authorization. - /// The cancellation token. - /// - /// A task with a result of the requested byte array. - /// - /// url. - /// Error GET Binary. - public static async Task GetBinary( - string url, - string authorization = null, - CancellationToken ct = default) - { - var response = await GetHttpContent(url, authorization, ct).ConfigureAwait(false); - - return await response.ReadAsByteArrayAsync().ConfigureAwait(false); - } - - /// - /// Authenticate against a web server using Bearer Token. - /// - /// The URL. - /// The username. - /// The password. - /// The cancellation token. - /// - /// A task with a Dictionary with authentication data. - /// - /// - /// url - /// or - /// username. - /// - /// Error Authenticating. - public static async Task> Authenticate( - string url, - string username, - string password, - CancellationToken ct = default) - { - if (string.IsNullOrWhiteSpace(url)) - throw new ArgumentNullException(nameof(url)); - - if (string.IsNullOrWhiteSpace(username)) - throw new ArgumentNullException(nameof(username)); - - using (var httpClient = new HttpClient()) - { - // ignore empty password for now - var requestContent = new StringContent( + : default) + : new OkOrError(); + } + } + + /// + /// Posts the specified URL. + /// + /// The URL. + /// The payload. + /// The authorization. + /// The cancellation token. + /// A task with a result as a collection of key/value pairs. + public static async Task> Post( + String url, + Object payload, + String authorization = null, + CancellationToken ct = default) { + String jsonString = await PostString(url, payload, authorization, ct).ConfigureAwait(false); + + return String.IsNullOrWhiteSpace(jsonString) + ? default + : Json.Deserialize(jsonString) as IDictionary; + } + + /// + /// Posts the specified URL. + /// + /// The URL. + /// The payload. + /// The authorization. + /// The cancellation token. + /// + /// A task with a result of the requested string. + /// + /// url. + /// Error POST JSON. + public static Task PostString( + String url, + Object payload, + String authorization = null, + CancellationToken ct = default) => SendAsync(HttpMethod.Post, url, payload, authorization, ct); + + /// + /// Puts the specified URL. + /// + /// The type of response object. + /// The URL. + /// The payload. + /// The authorization. + /// The cancellation token. + /// A task with a result of the requested type. + public static async Task Put( + String url, + Object payload, + String authorization = null, + CancellationToken ct = default) { + String jsonString = await PutString(url, payload, authorization, ct).ConfigureAwait(false); + + return !String.IsNullOrEmpty(jsonString) ? Json.Deserialize(jsonString) : default; + } + + /// + /// Puts the specified URL. + /// + /// The URL. + /// The payload. + /// The authorization. + /// The cancellation token. + /// A task with a result of the requested collection of key/value pairs. + public static async Task> Put( + String url, + Object payload, + String authorization = null, + CancellationToken ct = default) { + Object response = await Put(url, payload, authorization, ct).ConfigureAwait(false); + + return response as IDictionary; + } + + /// + /// Puts as string. + /// + /// The URL. + /// The payload. + /// The authorization. + /// The cancellation token. + /// + /// A task with a result of the requested string. + /// + /// url. + /// Error PUT JSON. + public static Task PutString( + String url, + Object payload, + String authorization = null, + CancellationToken ct = default) => SendAsync(HttpMethod.Put, url, payload, authorization, ct); + + /// + /// Gets as string. + /// + /// The URL. + /// The authorization. + /// The cancellation token. + /// + /// A task with a result of the requested string. + /// + /// url. + /// Error GET JSON. + public static async Task GetString( + String url, + String authorization = null, + CancellationToken ct = default) { + HttpContent response = await GetHttpContent(url, authorization, ct).ConfigureAwait(false); + + return await response.ReadAsStringAsync().ConfigureAwait(false); + } + + /// + /// Gets the specified URL and return the JSON data as object + /// with optional authorization token. + /// + /// The response type. + /// The URL. + /// The authorization. + /// The cancellation token. + /// A task with a result of the requested type. + public static async Task Get( + String url, + String authorization = null, + CancellationToken ct = default) { + String jsonString = await GetString(url, authorization, ct).ConfigureAwait(false); + + return !String.IsNullOrEmpty(jsonString) ? Json.Deserialize(jsonString) : default; + } + + /// + /// Gets the binary. + /// + /// The URL. + /// The authorization. + /// The cancellation token. + /// + /// A task with a result of the requested byte array. + /// + /// url. + /// Error GET Binary. + public static async Task GetBinary( + String url, + String authorization = null, + CancellationToken ct = default) { + HttpContent response = await GetHttpContent(url, authorization, ct).ConfigureAwait(false); + + return await response.ReadAsByteArrayAsync().ConfigureAwait(false); + } + + /// + /// Authenticate against a web server using Bearer Token. + /// + /// The URL. + /// The username. + /// The password. + /// The cancellation token. + /// + /// A task with a Dictionary with authentication data. + /// + /// + /// url + /// or + /// username. + /// + /// Error Authenticating. + public static async Task> Authenticate( + String url, + String username, + String password, + CancellationToken ct = default) { + if(String.IsNullOrWhiteSpace(url)) { + throw new ArgumentNullException(nameof(url)); + } + + if(String.IsNullOrWhiteSpace(username)) { + throw new ArgumentNullException(nameof(username)); + } + + using(HttpClient httpClient = new HttpClient()) { + // ignore empty password for now + StringContent requestContent = new StringContent( $"grant_type=password&username={username}&password={password}", Encoding.UTF8, - "application/x-www-form-urlencoded"); - var response = await httpClient.PostAsync(url, requestContent, ct).ConfigureAwait(false); - - if (response.IsSuccessStatusCode == false) - throw new SecurityException($"Error Authenticating. Status code: {response.StatusCode}."); - - var jsonPayload = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - - return Json.Deserialize(jsonPayload) as IDictionary; - } - } - - /// - /// Posts the file. - /// - /// The URL. - /// The buffer. - /// Name of the file. - /// The authorization. - /// The cancellation token. - /// - /// A task with a result of the requested string. - /// - public static Task PostFileString( - string url, - byte[] buffer, - string fileName, - string authorization = null, - CancellationToken ct = default) - { - return PostString(url, new {Filename = fileName, Data = buffer}, authorization, ct); - } - - /// - /// Posts the file. - /// - /// The response type. - /// The URL. - /// The buffer. - /// Name of the file. - /// The authorization. - /// The cancellation token. - /// A task with a result of the requested string. - public static Task PostFile( - string url, - byte[] buffer, - string fileName, - string authorization = null, - CancellationToken ct = default) - { - return Post(url, new {Filename = fileName, Data = buffer}, authorization, ct); - } - - /// - /// Sends the asynchronous request. - /// - /// The method. - /// The URL. - /// The payload. - /// The authorization. - /// The cancellation token. - /// A task with a result of the requested string. - public static async Task SendAsync(HttpMethod method, - string url, - object payload, - string authorization = null, - CancellationToken ct = default) - { - if (string.IsNullOrWhiteSpace(url)) - throw new ArgumentNullException(nameof(url)); - - using (var httpClient = GetHttpClientWithAuthorizationHeader(authorization)) - { - var payloadJson = new StringContent(Json.Serialize(payload), Encoding.UTF8, JsonMimeType); - - var response = await httpClient - .SendAsync(new HttpRequestMessage(method, url) {Content = payloadJson}, ct).ConfigureAwait(false); - - if (response.IsSuccessStatusCode == false) - { - throw new JsonRequestException( - $"Error {method} JSON", - (int) response.StatusCode, - await response.Content.ReadAsStringAsync().ConfigureAwait(false)); - } - - return await response.Content.ReadAsStringAsync().ConfigureAwait(false); - } - } - - private static HttpClient GetHttpClientWithAuthorizationHeader(string authorization) - { - var httpClient = new HttpClient(); - - if (string.IsNullOrWhiteSpace(authorization) == false) - { - httpClient.DefaultRequestHeaders.Authorization = - new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", authorization); - } - - return httpClient; - } - - private static async Task GetHttpContent( - string url, - string authorization, - CancellationToken ct) - { - if (string.IsNullOrWhiteSpace(url)) - throw new ArgumentNullException(nameof(url)); - - using (var httpClient = GetHttpClientWithAuthorizationHeader(authorization)) - { - var response = await httpClient.GetAsync(url, ct).ConfigureAwait(false); - - if (response.IsSuccessStatusCode == false) - throw new JsonRequestException("Error GET", (int) response.StatusCode); - - return response.Content; - } - } - } + "application/x-www-form-urlencoded"); + HttpResponseMessage response = await httpClient.PostAsync(url, requestContent, ct).ConfigureAwait(false); + + if(response.IsSuccessStatusCode == false) { + throw new SecurityException($"Error Authenticating. Status code: {response.StatusCode}."); + } + + String jsonPayload = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + return Json.Deserialize(jsonPayload) as IDictionary; + } + } + + /// + /// Posts the file. + /// + /// The URL. + /// The buffer. + /// Name of the file. + /// The authorization. + /// The cancellation token. + /// + /// A task with a result of the requested string. + /// + public static Task PostFileString( + String url, + Byte[] buffer, + String fileName, + String authorization = null, + CancellationToken ct = default) => PostString(url, new { + Filename = fileName, Data = buffer + }, authorization, ct); + + /// + /// Posts the file. + /// + /// The response type. + /// The URL. + /// The buffer. + /// Name of the file. + /// The authorization. + /// The cancellation token. + /// A task with a result of the requested string. + public static Task PostFile( + String url, + Byte[] buffer, + String fileName, + String authorization = null, + CancellationToken ct = default) => Post(url, new { + Filename = fileName, Data = buffer + }, authorization, ct); + + /// + /// Sends the asynchronous request. + /// + /// The method. + /// The URL. + /// The payload. + /// The authorization. + /// The cancellation token. + /// A task with a result of the requested string. + public static async Task SendAsync(HttpMethod method, + String url, + Object payload, + String authorization = null, + CancellationToken ct = default) { + if(String.IsNullOrWhiteSpace(url)) { + throw new ArgumentNullException(nameof(url)); + } + + using(HttpClient httpClient = GetHttpClientWithAuthorizationHeader(authorization)) { + StringContent payloadJson = new StringContent(Json.Serialize(payload), Encoding.UTF8, JsonMimeType); + + HttpResponseMessage response = await httpClient + .SendAsync(new HttpRequestMessage(method, url) { Content = payloadJson }, ct).ConfigureAwait(false); + + if(response.IsSuccessStatusCode == false) { + throw new JsonRequestException( + $"Error {method} JSON", + (Int32)response.StatusCode, + await response.Content.ReadAsStringAsync().ConfigureAwait(false)); + } + + return await response.Content.ReadAsStringAsync().ConfigureAwait(false); + } + } + + private static HttpClient GetHttpClientWithAuthorizationHeader(String authorization) { + HttpClient httpClient = new HttpClient(); + + if(String.IsNullOrWhiteSpace(authorization) == false) { + httpClient.DefaultRequestHeaders.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", authorization); + } + + return httpClient; + } + + private static async Task GetHttpContent( + String url, + String authorization, + CancellationToken ct) { + if(String.IsNullOrWhiteSpace(url)) { + throw new ArgumentNullException(nameof(url)); + } + + using(HttpClient httpClient = GetHttpClientWithAuthorizationHeader(authorization)) { + HttpResponseMessage response = await httpClient.GetAsync(url, ct).ConfigureAwait(false); + + if(response.IsSuccessStatusCode == false) { + throw new JsonRequestException("Error GET", (Int32)response.StatusCode); + } + + return response.Content; + } + } + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/Ldap/Asn1.cs b/Unosquare.Swan/Networking/Ldap/Asn1.cs index d2e0bc7..783ed63 100644 --- a/Unosquare.Swan/Networking/Ldap/Asn1.cs +++ b/Unosquare.Swan/Networking/Ldap/Asn1.cs @@ -1,621 +1,542 @@ -namespace Unosquare.Swan.Networking.Ldap -{ - using System; - using System.IO; - using System.Text; - +using System; +using System.IO; +using System.Text; + +namespace Unosquare.Swan.Networking.Ldap { + /// + /// The Asn1Set class can hold an unordered collection of components with + /// identical type. This class inherits from the Asn1Structured class + /// which already provides functionality to hold multiple Asn1 components. + /// + /// + internal class Asn1SetOf + : Asn1Structured { + public const Int32 Tag = 0x11; + + public static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, true, Tag); + + public Asn1SetOf(Int32 size = 10) + : base(Id, size) { + } + + public override String ToString() => this.ToString("SET OF: { "); + } + + /// + /// The Asn1Choice object represents the choice of any Asn1Object. All + /// Asn1Object methods are delegated to the object this Asn1Choice contains. + /// + /// + internal class Asn1Choice + : Asn1Object { + private Asn1Object _content; + + public Asn1Choice(Asn1Object content = null) => this._content = content; + + protected internal virtual Asn1Object ChoiceValue { + get => this._content; + set => this._content = value; + } + + public override Asn1Identifier GetIdentifier() => this._content.GetIdentifier(); + + public override void SetIdentifier(Asn1Identifier id) => this._content.SetIdentifier(id); + + public override String ToString() => this._content.ToString(); + } + + /// + /// This class is used to encapsulate an ASN.1 Identifier. + /// An Asn1Identifier is composed of three parts: + ///
  • a class type,
  • a form, and
  • a tag.
  • + /// The class type is defined as: + ///
    +  /// bit 8 7 TAG CLASS
    +  /// ------- -----------
    +  /// 0 0 UNIVERSAL
    +  /// 0 1 APPLICATION
    +  /// 1 0 CONTEXT
    +  /// 1 1 PRIVATE
    +  /// 
    + /// The form is defined as: + ///
    +  /// bit 6 FORM
    +  /// ----- --------
    +  /// 0 PRIMITIVE
    +  /// 1 CONSTRUCTED
    +  /// 
    + /// Note: CONSTRUCTED types are made up of other CONSTRUCTED or PRIMITIVE + /// types. + /// The tag is defined as:. + ///
    +  /// bit 5 4 3 2 1 TAG
    +  /// ------------- ---------------------------------------------
    +  /// 0 0 0 0 0
    +  /// . . . . .
    +  /// 1 1 1 1 0 (0-30) single octet tag
    +  /// 1 1 1 1 1 (> 30) multiple octet tag, more octets follow
    +  /// 
    + internal sealed class Asn1Identifier { + public Asn1Identifier(Asn1IdentifierTag tagClass, Boolean constructed, Int32 tag) { + this.Asn1Class = tagClass; + this.Constructed = constructed; + this.Tag = tag; + } + + public Asn1Identifier(LdapOperation tag) + : this(Asn1IdentifierTag.Application, true, (Int32)tag) { + } + + public Asn1Identifier(Int32 contextTag, Boolean constructed = false) + : this(Asn1IdentifierTag.Context, constructed, contextTag) { + } + + public Asn1Identifier(Stream stream) { + Int32 r = stream.ReadByte(); + this.EncodedLength++; + + if(r < 0) { + throw new EndOfStreamException("BERDecoder: decode: EOF in Identifier"); + } + + this.Asn1Class = (Asn1IdentifierTag)(r >> 6); + this.Constructed = (r & 0x20) != 0; + this.Tag = r & 0x1F; // if tag < 30 then its a single octet identifier. + + if(this.Tag == 0x1F) { + // if true, its a multiple octet identifier. + this.Tag = this.DecodeTagNumber(stream); + } + } + + public Asn1IdentifierTag Asn1Class { + get; + } + + public Boolean Constructed { + get; + } + + public Int32 Tag { + get; + } + + public Int32 EncodedLength { + get; private set; + } + + public Boolean Universal => this.Asn1Class == Asn1IdentifierTag.Universal; + + public Object Clone() => this.MemberwiseClone(); + + private Int32 DecodeTagNumber(Stream stream) { + Int32 n = 0; + while(true) { + Int32 r = stream.ReadByte(); + this.EncodedLength++; + if(r < 0) { + throw new EndOfStreamException("BERDecoder: decode: EOF in tag number"); + } + + n = (n << 7) + (r & 0x7F); + if((r & 0x80) == 0) { + break; + } + } + + return n; + } + } + + /// + /// This is the base class for all other Asn1 types. + /// + internal abstract class Asn1Object { + private static readonly String[] ClassTypes = { "[UNIVERSAL ", "[APPLICATION ", "[", "[PRIVATE " }; + + private Asn1Identifier _id; + + protected Asn1Object(Asn1Identifier id = null) => this._id = id; + + public virtual Asn1Identifier GetIdentifier() => this._id; + + public virtual void SetIdentifier(Asn1Identifier identifier) => this._id = identifier; + + public override String ToString() { + Asn1Identifier identifier = this.GetIdentifier(); + + return $"{ClassTypes[(Int32)identifier.Asn1Class]}{identifier.Tag}]"; + } + } + + /// + /// This class encapsulates the OCTET STRING type. + /// + /// + internal sealed class Asn1OctetString + : Asn1Object { + public const Int32 Tag = 0x04; + + private static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, false, Tag); + + private readonly SByte[] _content; + + public Asn1OctetString(SByte[] content) + : base(Id) => this._content = content; + + public Asn1OctetString(String content) + : base(Id) => this._content = Encoding.UTF8.GetSBytes(content); + + public Asn1OctetString(Stream stream, Int32 len) + : base(Id) => this._content = len > 0 ? (SByte[])LberDecoder.DecodeOctetString(stream, len) : new SByte[0]; + + public SByte[] ByteValue() => this._content; + + public String StringValue() => Encoding.UTF8.GetString(this._content); + + public override String ToString() => base.ToString() + "OCTET STRING: " + this.StringValue(); + } + + /// + /// The Asn1Tagged class can hold a base Asn1Object with a distinctive tag + /// describing the type of that base object. It also maintains a boolean value + /// indicating whether the value should be encoded by EXPLICIT or IMPLICIT + /// means. (Explicit is true by default.) + /// If the type is encoded IMPLICITLY, the base types form, length and content + /// will be encoded as usual along with the class type and tag specified in + /// the constructor of this Asn1Tagged class. + /// If the type is to be encoded EXPLICITLY, the base type will be encoded as + /// usual after the Asn1Tagged identifier has been encoded. + /// + /// + internal class Asn1Tagged : Asn1Object { + private Asn1Object _content; + + public Asn1Tagged(Asn1Identifier identifier, Asn1Object obj = null, Boolean isExplicit = true) + : base(identifier) { + this._content = obj; + this.Explicit = isExplicit; + + if(!isExplicit) { + // replace object's id with new tag. + this._content?.SetIdentifier(identifier); + } + } + + public Asn1Tagged(Asn1Identifier identifier, SByte[] vals) + : base(identifier) { + this._content = new Asn1OctetString(vals); + this.Explicit = false; + } + + public Asn1Tagged(Stream stream, Int32 len, Asn1Identifier identifier) + : base(identifier) => + // If we are decoding an implicit tag, there is no way to know at this + // low level what the base type really is. We can place the content + // into an Asn1OctetString type and pass it back to the application who + // will be able to create the appropriate ASN.1 type for this tag. + this._content = new Asn1OctetString(stream, len); + + public Asn1Object TaggedValue { + get => this._content; + + set { + this._content = value; + if(!this.Explicit) { + // replace object's id with new tag. + value?.SetIdentifier(this.GetIdentifier()); + } + } + } + + public Boolean Explicit { + get; + } + + public override String ToString() => this.Explicit ? base.ToString() + this._content : this._content.ToString(); + } + + /// + /// This class serves as the base type for all ASN.1 + /// structured types. + /// + /// + internal abstract class Asn1Structured : Asn1Object { + private Asn1Object[] _content; + private Int32 _contentIndex; + + protected internal Asn1Structured(Asn1Identifier id, Int32 size = 10) + : base(id) => this._content = new Asn1Object[size]; + + public Asn1Object[] ToArray() { + Asn1Object[] cloneArray = new Asn1Object[this._contentIndex]; + Array.Copy(this._content, 0, cloneArray, 0, this._contentIndex); + return cloneArray; + } + + public void Add(String s) => this.Add(new Asn1OctetString(s)); + + public void Add(Asn1Object obj) { + if(this._contentIndex == this._content.Length) { + // Array too small, need to expand it, double length + Asn1Object[] newArray = new Asn1Object[this._contentIndex + this._contentIndex]; + Array.Copy(this._content, 0, newArray, 0, this._contentIndex); + this._content = newArray; + } + + this._content[this._contentIndex++] = obj; + } + + public void Set(Int32 index, Asn1Object value) { + if(index >= this._contentIndex || index < 0) { + throw new IndexOutOfRangeException($"Asn1Structured: get: index {index}, size {this._contentIndex}"); + } + + this._content[index] = value; + } + + public Asn1Object Get(Int32 index) { + if(index >= this._contentIndex || index < 0) { + throw new IndexOutOfRangeException($"Asn1Structured: set: index {index}, size {this._contentIndex}"); + } + + return this._content[index]; + } + + public Int32 Size() => this._contentIndex; + + public String ToString(String type) { + StringBuilder sb = new StringBuilder().Append(type); + + for(Int32 i = 0; i < this._contentIndex; i++) { + _ = sb.Append(this._content[i]); + if(i != this._contentIndex - 1) { + _ = sb.Append(", "); + } + } + + _ = sb.Append(" }"); + + return base.ToString() + sb; + } + + protected internal void DecodeStructured(Stream stream, Int32 len) { + Int32[] componentLen = new Int32[1]; // collects length of component + + while(len > 0) { + this.Add(LberDecoder.Decode(stream, componentLen)); + len -= componentLen[0]; + } + } + } + + /// + /// This class encapsulates the ASN.1 BOOLEAN type. + /// + /// + internal class Asn1Boolean + : Asn1Object { + public const Int32 Tag = 0x01; + + public static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, false, Tag); + + private readonly Boolean _content; + + public Asn1Boolean(Boolean content) + : base(Id) => this._content = content; + + public Asn1Boolean(Stream stream, Int32 len) + : base(Id) => this._content = LberDecoder.DecodeBoolean(stream, len); + + public Boolean BooleanValue() => this._content; + + public override String ToString() => $"{base.ToString()}BOOLEAN: {this._content}"; + } + + /// + /// This class represents the ASN.1 NULL type. + /// + /// + internal sealed class Asn1Null + : Asn1Object { + public const Int32 Tag = 0x05; + + public static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, false, Tag); + + public Asn1Null() + : base(Id) { + } + + public override String ToString() => $"{base.ToString()}NULL: \"\""; + } + + /// + /// This abstract class is the base class + /// for all Asn1 numeric (integral) types. These include + /// Asn1Integer and Asn1Enumerated. + /// + /// + internal abstract class Asn1Numeric : Asn1Object { + private readonly Int64 _content; + + internal Asn1Numeric(Asn1Identifier id, Int32 numericValue) + : base(id) => this._content = numericValue; + + internal Asn1Numeric(Asn1Identifier id, Int64 numericValue) + : base(id) => this._content = numericValue; + + public Int32 IntValue() => (Int32)this._content; + + public Int64 LongValue() => this._content; + } + + /// + /// This class provides a means to manipulate ASN.1 Length's. It will + /// be used by Asn1Encoder's and Asn1Decoder's by composition. + /// + internal sealed class Asn1Length { + public Asn1Length(Stream stream) { + Int32 r = stream.ReadByte(); + this.EncodedLength++; + if(r == 0x80) { + this.Length = -1; + } else if(r < 0x80) { + this.Length = r; + } else { + this.Length = 0; + for(r &= 0x7F; r > 0; r--) { + Int32 part = stream.ReadByte(); + this.EncodedLength++; + if(part < 0) { + throw new EndOfStreamException("BERDecoder: decode: EOF in Asn1Length"); + } + + this.Length = (this.Length << 8) + part; + } + } + } + + public Int32 Length { + get; + } + + public Int32 EncodedLength { + get; + } + } + + /// + /// The Asn1Sequence class can hold an ordered collection of components with + /// distinct type. + /// This class inherits from the Asn1Structured class which + /// provides functionality to hold multiple Asn1 components. + /// + /// + internal class Asn1Sequence + : Asn1Structured { + public const Int32 Tag = 0x10; + + private static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, true, Tag); + + public Asn1Sequence(Int32 size) + : base(Id, size) { + } + + public Asn1Sequence(Stream stream, Int32 len) + : base(Id) => this.DecodeStructured(stream, len); + + public override String ToString() => this.ToString("SEQUENCE: { "); + } + + /// + /// The Asn1Set class can hold an unordered collection of components with + /// distinct type. This class inherits from the Asn1Structured class + /// which already provides functionality to hold multiple Asn1 components. + /// + /// + internal sealed class Asn1Set + : Asn1Structured { + public const Int32 Tag = 0x11; + + public static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, true, Tag); + + public Asn1Set(Stream stream, Int32 len) + : base(Id) => this.DecodeStructured(stream, len); + + public override String ToString() => this.ToString("SET: { "); + } + + /// + /// This class encapsulates the ASN.1 INTEGER type. + /// + /// + internal class Asn1Integer + : Asn1Numeric { + public const Int32 Tag = 0x02; + + public static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, false, Tag); + + public Asn1Integer(Int32 content) + : base(Id, content) { + } + + public Asn1Integer(Stream stream, Int32 len) + : base(Id, LberDecoder.DecodeNumeric(stream, len)) { + } + + public override String ToString() => base.ToString() + "INTEGER: " + this.LongValue(); + } + + /// + /// This class encapsulates the ASN.1 ENUMERATED type. + /// + /// + internal sealed class Asn1Enumerated : Asn1Numeric { + public const Int32 Tag = 0x0a; + + public static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, false, Tag); + + public Asn1Enumerated(LdapScope content) + : base(Id, (Int32)content) { + } + + public Asn1Enumerated(Int32 content) + : base(Id, content) { + } + /// - /// The Asn1Set class can hold an unordered collection of components with - /// identical type. This class inherits from the Asn1Structured class - /// which already provides functionality to hold multiple Asn1 components. + /// Initializes a new instance of the class. + /// Constructs an Asn1Enumerated object by decoding data from an + /// input stream. /// - /// - internal class Asn1SetOf - : Asn1Structured - { - public const int Tag = 0x11; - - public static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, true, Tag); - - public Asn1SetOf(int size = 10) - : base(Id, size) - { - } - - public override string ToString() => ToString("SET OF: { "); - } - - /// - /// The Asn1Choice object represents the choice of any Asn1Object. All - /// Asn1Object methods are delegated to the object this Asn1Choice contains. - /// - /// - internal class Asn1Choice - : Asn1Object - { - private Asn1Object _content; - - public Asn1Choice(Asn1Object content = null) - { - _content = content; - } - - protected internal virtual Asn1Object ChoiceValue - { - get => _content; - set => _content = value; - } - - public override Asn1Identifier GetIdentifier() => _content.GetIdentifier(); - - public override void SetIdentifier(Asn1Identifier id) => _content.SetIdentifier(id); - - public override string ToString() => _content.ToString(); - } - - /// - /// This class is used to encapsulate an ASN.1 Identifier. - /// An Asn1Identifier is composed of three parts: - ///
  • a class type,
  • a form, and
  • a tag.
  • - /// The class type is defined as: - ///
    -    /// bit 8 7 TAG CLASS
    -    /// ------- -----------
    -    /// 0 0 UNIVERSAL
    -    /// 0 1 APPLICATION
    -    /// 1 0 CONTEXT
    -    /// 1 1 PRIVATE
    -    /// 
    - /// The form is defined as: - ///
    -    /// bit 6 FORM
    -    /// ----- --------
    -    /// 0 PRIMITIVE
    -    /// 1 CONSTRUCTED
    -    /// 
    - /// Note: CONSTRUCTED types are made up of other CONSTRUCTED or PRIMITIVE - /// types. - /// The tag is defined as:. - ///
    -    /// bit 5 4 3 2 1 TAG
    -    /// ------------- ---------------------------------------------
    -    /// 0 0 0 0 0
    -    /// . . . . .
    -    /// 1 1 1 1 0 (0-30) single octet tag
    -    /// 1 1 1 1 1 (> 30) multiple octet tag, more octets follow
    -    /// 
    - internal sealed class Asn1Identifier - { - public Asn1Identifier(Asn1IdentifierTag tagClass, bool constructed, int tag) - { - Asn1Class = tagClass; - Constructed = constructed; - Tag = tag; - } - - public Asn1Identifier(LdapOperation tag) - : this(Asn1IdentifierTag.Application, true, (int) tag) - { - } - - public Asn1Identifier(int contextTag, bool constructed = false) - : this(Asn1IdentifierTag.Context, constructed, contextTag) - { - } - - public Asn1Identifier(Stream stream) - { - var r = stream.ReadByte(); - EncodedLength++; - - if (r < 0) - throw new EndOfStreamException("BERDecoder: decode: EOF in Identifier"); - - Asn1Class = (Asn1IdentifierTag) (r >> 6); - Constructed = (r & 0x20) != 0; - Tag = r & 0x1F; // if tag < 30 then its a single octet identifier. - - if (Tag == 0x1F) - { - // if true, its a multiple octet identifier. - Tag = DecodeTagNumber(stream); - } - } - - public Asn1IdentifierTag Asn1Class { get; } - - public bool Constructed { get; } - - public int Tag { get; } - - public int EncodedLength { get; private set; } - - public bool Universal => Asn1Class == Asn1IdentifierTag.Universal; - - public object Clone() => MemberwiseClone(); - - private int DecodeTagNumber(Stream stream) - { - var n = 0; - while (true) - { - var r = stream.ReadByte(); - EncodedLength++; - if (r < 0) - throw new EndOfStreamException("BERDecoder: decode: EOF in tag number"); - - n = (n << 7) + (r & 0x7F); - if ((r & 0x80) == 0) break; - } - - return n; - } - } - - /// - /// This is the base class for all other Asn1 types. - /// - internal abstract class Asn1Object - { - private static readonly string[] ClassTypes = {"[UNIVERSAL ", "[APPLICATION ", "[", "[PRIVATE "}; - - private Asn1Identifier _id; - - protected Asn1Object(Asn1Identifier id = null) - { - _id = id; - } - - public virtual Asn1Identifier GetIdentifier() => _id; - - public virtual void SetIdentifier(Asn1Identifier identifier) => _id = identifier; - - public override string ToString() - { - var identifier = GetIdentifier(); - - return $"{ClassTypes[(int) identifier.Asn1Class]}{identifier.Tag}]"; - } - } - - /// - /// This class encapsulates the OCTET STRING type. - /// - /// - internal sealed class Asn1OctetString - : Asn1Object - { - public const int Tag = 0x04; - - private static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, false, Tag); - - private readonly sbyte[] _content; - - public Asn1OctetString(sbyte[] content) - : base(Id) - { - _content = content; - } - - public Asn1OctetString(string content) - : base(Id) - { - _content = Encoding.UTF8.GetSBytes(content); - } - - public Asn1OctetString(Stream stream, int len) - : base(Id) - { - _content = len > 0 ? (sbyte[]) LberDecoder.DecodeOctetString(stream, len) : new sbyte[0]; - } - - public sbyte[] ByteValue() => _content; - - public string StringValue() => Encoding.UTF8.GetString(_content); - - public override string ToString() => base.ToString() + "OCTET STRING: " + StringValue(); - } - - /// - /// The Asn1Tagged class can hold a base Asn1Object with a distinctive tag - /// describing the type of that base object. It also maintains a boolean value - /// indicating whether the value should be encoded by EXPLICIT or IMPLICIT - /// means. (Explicit is true by default.) - /// If the type is encoded IMPLICITLY, the base types form, length and content - /// will be encoded as usual along with the class type and tag specified in - /// the constructor of this Asn1Tagged class. - /// If the type is to be encoded EXPLICITLY, the base type will be encoded as - /// usual after the Asn1Tagged identifier has been encoded. - /// - /// - internal class Asn1Tagged : Asn1Object - { - private Asn1Object _content; - - public Asn1Tagged(Asn1Identifier identifier, Asn1Object obj = null, bool isExplicit = true) - : base(identifier) - { - _content = obj; - Explicit = isExplicit; - - if (!isExplicit) - { - // replace object's id with new tag. - _content?.SetIdentifier(identifier); - } - } - - public Asn1Tagged(Asn1Identifier identifier, sbyte[] vals) - : base(identifier) - { - _content = new Asn1OctetString(vals); - Explicit = false; - } - - public Asn1Tagged(Stream stream, int len, Asn1Identifier identifier) - : base(identifier) - { - // If we are decoding an implicit tag, there is no way to know at this - // low level what the base type really is. We can place the content - // into an Asn1OctetString type and pass it back to the application who - // will be able to create the appropriate ASN.1 type for this tag. - _content = new Asn1OctetString(stream, len); - } - - public Asn1Object TaggedValue - { - get => _content; - - set - { - _content = value; - if (!Explicit) - { - // replace object's id with new tag. - value?.SetIdentifier(GetIdentifier()); - } - } - } - - public bool Explicit { get; } - - public override string ToString() => Explicit ? base.ToString() + _content : _content.ToString(); - } - - /// - /// This class serves as the base type for all ASN.1 - /// structured types. - /// - /// - internal abstract class Asn1Structured : Asn1Object - { - private Asn1Object[] _content; - private int _contentIndex; - - protected internal Asn1Structured(Asn1Identifier id, int size = 10) - : base(id) - { - _content = new Asn1Object[size]; - } - - public Asn1Object[] ToArray() - { - var cloneArray = new Asn1Object[_contentIndex]; - Array.Copy(_content, 0, cloneArray, 0, _contentIndex); - return cloneArray; - } - - public void Add(string s) => Add(new Asn1OctetString(s)); - - public void Add(Asn1Object obj) - { - if (_contentIndex == _content.Length) - { - // Array too small, need to expand it, double length - var newArray = new Asn1Object[_contentIndex + _contentIndex]; - Array.Copy(_content, 0, newArray, 0, _contentIndex); - _content = newArray; - } - - _content[_contentIndex++] = obj; - } - - public void Set(int index, Asn1Object value) - { - if (index >= _contentIndex || index < 0) - { - throw new IndexOutOfRangeException($"Asn1Structured: get: index {index}, size {_contentIndex}"); - } - - _content[index] = value; - } - - public Asn1Object Get(int index) - { - if (index >= _contentIndex || index < 0) - { - throw new IndexOutOfRangeException($"Asn1Structured: set: index {index}, size {_contentIndex}"); - } - - return _content[index]; - } - - public int Size() => _contentIndex; - - public string ToString(string type) - { - var sb = new StringBuilder().Append(type); - - for (var i = 0; i < _contentIndex; i++) - { - sb.Append(_content[i]); - if (i != _contentIndex - 1) - sb.Append(", "); - } - - sb.Append(" }"); - - return base.ToString() + sb; - } - - protected internal void DecodeStructured(Stream stream, int len) - { - var componentLen = new int[1]; // collects length of component - - while (len > 0) - { - Add(LberDecoder.Decode(stream, componentLen)); - len -= componentLen[0]; - } - } - } - - /// - /// This class encapsulates the ASN.1 BOOLEAN type. - /// - /// - internal class Asn1Boolean - : Asn1Object - { - public const int Tag = 0x01; - - public static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, false, Tag); - - private readonly bool _content; - - public Asn1Boolean(bool content) - : base(Id) - { - _content = content; - } - - public Asn1Boolean(Stream stream, int len) - : base(Id) - { - _content = LberDecoder.DecodeBoolean(stream, len); - } - - public bool BooleanValue() => _content; - - public override string ToString() => $"{base.ToString()}BOOLEAN: {_content}"; - } - - /// - /// This class represents the ASN.1 NULL type. - /// - /// - internal sealed class Asn1Null - : Asn1Object - { - public const int Tag = 0x05; - - public static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, false, Tag); - - public Asn1Null() - : base(Id) - { - } - - public override string ToString() => $"{base.ToString()}NULL: \"\""; - } - - /// - /// This abstract class is the base class - /// for all Asn1 numeric (integral) types. These include - /// Asn1Integer and Asn1Enumerated. - /// - /// - internal abstract class Asn1Numeric : Asn1Object - { - private readonly long _content; - - internal Asn1Numeric(Asn1Identifier id, int numericValue) - : base(id) - { - _content = numericValue; - } - - internal Asn1Numeric(Asn1Identifier id, long numericValue) - : base(id) - { - _content = numericValue; - } - - public int IntValue() => (int) _content; - - public long LongValue() => _content; - } - - /// - /// This class provides a means to manipulate ASN.1 Length's. It will - /// be used by Asn1Encoder's and Asn1Decoder's by composition. - /// - internal sealed class Asn1Length - { - public Asn1Length(Stream stream) - { - var r = stream.ReadByte(); - EncodedLength++; - if (r == 0x80) - { - Length = -1; - } - else if (r < 0x80) - { - Length = r; - } - else - { - Length = 0; - for (r = r & 0x7F; r > 0; r--) - { - var part = stream.ReadByte(); - EncodedLength++; - if (part < 0) - throw new EndOfStreamException("BERDecoder: decode: EOF in Asn1Length"); - Length = (Length << 8) + part; - } - } - } - - public int Length { get; } - - public int EncodedLength { get; } - } - - /// - /// The Asn1Sequence class can hold an ordered collection of components with - /// distinct type. - /// This class inherits from the Asn1Structured class which - /// provides functionality to hold multiple Asn1 components. - /// - /// - internal class Asn1Sequence - : Asn1Structured - { - public const int Tag = 0x10; - - private static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, true, Tag); - - public Asn1Sequence(int size) - : base(Id, size) - { - } - - public Asn1Sequence(Stream stream, int len) - : base(Id) - { - DecodeStructured(stream, len); - } - - public override string ToString() => ToString("SEQUENCE: { "); - } - - /// - /// The Asn1Set class can hold an unordered collection of components with - /// distinct type. This class inherits from the Asn1Structured class - /// which already provides functionality to hold multiple Asn1 components. - /// - /// - internal sealed class Asn1Set - : Asn1Structured - { - public const int Tag = 0x11; - - public static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, true, Tag); - - public Asn1Set(Stream stream, int len) - : base(Id) - { - DecodeStructured(stream, len); - } - - public override string ToString() => ToString("SET: { "); - } - - /// - /// This class encapsulates the ASN.1 INTEGER type. - /// - /// - internal class Asn1Integer - : Asn1Numeric - { - public const int Tag = 0x02; - - public static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, false, Tag); - - public Asn1Integer(int content) - : base(Id, content) - { - } - - public Asn1Integer(Stream stream, int len) - : base(Id, LberDecoder.DecodeNumeric(stream, len)) - { - } - - public override string ToString() => base.ToString() + "INTEGER: " + LongValue(); - } - - /// - /// This class encapsulates the ASN.1 ENUMERATED type. - /// - /// - internal sealed class Asn1Enumerated : Asn1Numeric - { - public const int Tag = 0x0a; - - public static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, false, Tag); - - public Asn1Enumerated(LdapScope content) - : base(Id, (int) content) - { - } - - public Asn1Enumerated(int content) - : base(Id, content) - { - } - - /// - /// Initializes a new instance of the class. - /// Constructs an Asn1Enumerated object by decoding data from an - /// input stream. - /// - /// The stream. - /// The length. - public Asn1Enumerated(Stream stream, int len) - : base(Id, LberDecoder.DecodeNumeric(stream, len)) - { - } - - public override string ToString() => base.ToString() + "ENUMERATED: " + LongValue(); - } - - /// - /// The Asn1SequenceOf class is used to hold an ordered collection - /// of components with identical type. This class inherits - /// from the Asn1Structured class which already provides - /// functionality to hold multiple Asn1 components. - /// - /// - internal class Asn1SequenceOf : Asn1Structured - { - public const int Tag = 0x10; - - public static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, true, Tag); - - public Asn1SequenceOf(int size) - : base(Id, size) - { - } - - public Asn1SequenceOf(Stream stream, int len) - : base(Id) - { - DecodeStructured(stream, len); - } - - public override string ToString() => ToString("SEQUENCE OF: { "); - } + /// The stream. + /// The length. + public Asn1Enumerated(Stream stream, Int32 len) + : base(Id, LberDecoder.DecodeNumeric(stream, len)) { + } + + public override String ToString() => base.ToString() + "ENUMERATED: " + this.LongValue(); + } + + /// + /// The Asn1SequenceOf class is used to hold an ordered collection + /// of components with identical type. This class inherits + /// from the Asn1Structured class which already provides + /// functionality to hold multiple Asn1 components. + /// + /// + internal class Asn1SequenceOf : Asn1Structured { + public const Int32 Tag = 0x10; + + public static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, true, Tag); + + public Asn1SequenceOf(Int32 size) + : base(Id, size) { + } + + public Asn1SequenceOf(Stream stream, Int32 len) + : base(Id) => this.DecodeStructured(stream, len); + + public override String ToString() => this.ToString("SEQUENCE OF: { "); + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/Ldap/LberDecoder.cs b/Unosquare.Swan/Networking/Ldap/LberDecoder.cs index 1b2892b..d3498c7 100644 --- a/Unosquare.Swan/Networking/Ldap/LberDecoder.cs +++ b/Unosquare.Swan/Networking/Ldap/LberDecoder.cs @@ -1,166 +1,165 @@ -namespace Unosquare.Swan.Networking.Ldap -{ - using System.IO; - +using System.IO; +using System; + +namespace Unosquare.Swan.Networking.Ldap { + /// + /// This class provides LBER decoding routines for ASN.1 Types. LBER is a + /// subset of BER as described in the following taken from 5.1 of RFC 2251: + /// 5.1. Mapping Onto BER-based Transport Services + /// The protocol elements of Ldap are encoded for exchange using the + /// Basic Encoding Rules (BER) [11] of ASN.1 [3]. However, due to the + /// high overhead involved in using certain elements of the BER, the + /// following additional restrictions are placed on BER-encodings of Ldap + /// protocol elements: + ///
  • (1) Only the definite form of length encoding will be used.
  • + ///
  • (2) OCTET STRING values will be encoded in the primitive form only.
  • + /// (3) If the value of a BOOLEAN type is true, the encoding MUST have + /// its contents octets set to hex "FF". + ///
  • + /// (4) If a value of a type is its default value, it MUST be absent. + /// Only some BOOLEAN and INTEGER types have default values in this + /// protocol definition. + /// These restrictions do not apply to ASN.1 types encapsulated inside of + /// OCTET STRING values, such as attribute values, unless otherwise + /// noted. + ///
  • + /// [3] ITU-T Rec. X.680, "Abstract Syntax Notation One (ASN.1) - + /// Specification of Basic Notation", 1994. + /// [11] ITU-T Rec. X.690, "Specification of ASN.1 encoding rules: Basic, + /// Canonical, and Distinguished Encoding Rules", 1994. + ///
    + internal static class LberDecoder { /// - /// This class provides LBER decoding routines for ASN.1 Types. LBER is a - /// subset of BER as described in the following taken from 5.1 of RFC 2251: - /// 5.1. Mapping Onto BER-based Transport Services - /// The protocol elements of Ldap are encoded for exchange using the - /// Basic Encoding Rules (BER) [11] of ASN.1 [3]. However, due to the - /// high overhead involved in using certain elements of the BER, the - /// following additional restrictions are placed on BER-encodings of Ldap - /// protocol elements: - ///
  • (1) Only the definite form of length encoding will be used.
  • - ///
  • (2) OCTET STRING values will be encoded in the primitive form only.
  • - /// (3) If the value of a BOOLEAN type is true, the encoding MUST have - /// its contents octets set to hex "FF". - ///
  • - /// (4) If a value of a type is its default value, it MUST be absent. - /// Only some BOOLEAN and INTEGER types have default values in this - /// protocol definition. - /// These restrictions do not apply to ASN.1 types encapsulated inside of - /// OCTET STRING values, such as attribute values, unless otherwise - /// noted. - ///
  • - /// [3] ITU-T Rec. X.680, "Abstract Syntax Notation One (ASN.1) - - /// Specification of Basic Notation", 1994. - /// [11] ITU-T Rec. X.690, "Specification of ASN.1 encoding rules: Basic, - /// Canonical, and Distinguished Encoding Rules", 1994. + /// Decode an LBER encoded value into an Asn1Object from an InputStream. + /// This method also returns the total length of this encoded + /// Asn1Object (length of type + length of length + length of content) + /// in the parameter len. This information is helpful when decoding + /// structured types. ///
    - internal static class LberDecoder - { - /// - /// Decode an LBER encoded value into an Asn1Object from an InputStream. - /// This method also returns the total length of this encoded - /// Asn1Object (length of type + length of length + length of content) - /// in the parameter len. This information is helpful when decoding - /// structured types. - /// - /// The stream. - /// The length. - /// - /// Decoded Asn1Obect. - /// - /// Unknown tag. - public static Asn1Object Decode(Stream stream, int[] len) - { - var asn1Id = new Asn1Identifier(stream); - var asn1Len = new Asn1Length(stream); - - var length = asn1Len.Length; - len[0] = asn1Id.EncodedLength + asn1Len.EncodedLength + length; - - if (asn1Id.Universal == false) - return new Asn1Tagged(stream, length, (Asn1Identifier) asn1Id.Clone()); - - switch (asn1Id.Tag) - { - case Asn1Sequence.Tag: - return new Asn1Sequence(stream, length); - - case Asn1Set.Tag: - return new Asn1Set(stream, length); - - case Asn1Boolean.Tag: - return new Asn1Boolean(stream, length); - - case Asn1Integer.Tag: - return new Asn1Integer(stream, length); - - case Asn1OctetString.Tag: - return new Asn1OctetString(stream, length); - - case Asn1Enumerated.Tag: - return new Asn1Enumerated(stream, length); - - case Asn1Null.Tag: - return new Asn1Null(); // has no content to decode. - - default: - throw new EndOfStreamException("Unknown tag"); - } - } - - /// - /// Decode a boolean directly from a stream. - /// - /// The stream. - /// Length in bytes. - /// - /// Decoded boolean object. - /// - /// LBER: BOOLEAN: decode error: EOF. - public static bool DecodeBoolean(Stream stream, int len) - { - var lber = new sbyte[len]; - - if (stream.ReadInput(ref lber, 0, lber.Length) != len) - throw new EndOfStreamException("LBER: BOOLEAN: decode error: EOF"); - - return lber[0] != 0x00; - } - - /// - /// Decode a Numeric type directly from a stream. Decodes INTEGER - /// and ENUMERATED types. - /// - /// The stream. - /// Length in bytes. - /// - /// Decoded numeric object. - /// - /// - /// LBER: NUMERIC: decode error: EOF - /// or - /// LBER: NUMERIC: decode error: EOF. - /// - public static long DecodeNumeric(Stream stream, int len) - { - long l = 0; - var r = stream.ReadByte(); - - if (r < 0) - throw new EndOfStreamException("LBER: NUMERIC: decode error: EOF"); - - if ((r & 0x80) != 0) - { - // check for negative number - l = -1; - } - - l = (l << 8) | r; - - for (var i = 1; i < len; i++) - { - r = stream.ReadByte(); - if (r < 0) - throw new EndOfStreamException("LBER: NUMERIC: decode error: EOF"); - - l = (l << 8) | r; - } - - return l; - } - - /// - /// Decode an OctetString directly from a stream. - /// - /// The stream. - /// Length in bytes. - /// Decoded octet. - public static object DecodeOctetString(Stream stream, int len) - { - var octets = new sbyte[len]; - var totalLen = 0; - - while (totalLen < len) - { - // Make sure we have read all the data - totalLen += stream.ReadInput(ref octets, totalLen, len - totalLen); - } - - return octets; - } - } + /// The stream. + /// The length. + /// + /// Decoded Asn1Obect. + /// + /// Unknown tag. + public static Asn1Object Decode(Stream stream, Int32[] len) { + Asn1Identifier asn1Id = new Asn1Identifier(stream); + Asn1Length asn1Len = new Asn1Length(stream); + + Int32 length = asn1Len.Length; + len[0] = asn1Id.EncodedLength + asn1Len.EncodedLength + length; + + if(asn1Id.Universal == false) { + return new Asn1Tagged(stream, length, (Asn1Identifier)asn1Id.Clone()); + } + + switch(asn1Id.Tag) { + case Asn1Sequence.Tag: + return new Asn1Sequence(stream, length); + + case Asn1Set.Tag: + return new Asn1Set(stream, length); + + case Asn1Boolean.Tag: + return new Asn1Boolean(stream, length); + + case Asn1Integer.Tag: + return new Asn1Integer(stream, length); + + case Asn1OctetString.Tag: + return new Asn1OctetString(stream, length); + + case Asn1Enumerated.Tag: + return new Asn1Enumerated(stream, length); + + case Asn1Null.Tag: + return new Asn1Null(); // has no content to decode. + + default: + throw new EndOfStreamException("Unknown tag"); + } + } + + /// + /// Decode a boolean directly from a stream. + /// + /// The stream. + /// Length in bytes. + /// + /// Decoded boolean object. + /// + /// LBER: BOOLEAN: decode error: EOF. + public static Boolean DecodeBoolean(Stream stream, Int32 len) { + SByte[] lber = new SByte[len]; + + if(stream.ReadInput(ref lber, 0, lber.Length) != len) { + throw new EndOfStreamException("LBER: BOOLEAN: decode error: EOF"); + } + + return lber[0] != 0x00; + } + + /// + /// Decode a Numeric type directly from a stream. Decodes INTEGER + /// and ENUMERATED types. + /// + /// The stream. + /// Length in bytes. + /// + /// Decoded numeric object. + /// + /// + /// LBER: NUMERIC: decode error: EOF + /// or + /// LBER: NUMERIC: decode error: EOF. + /// + public static Int64 DecodeNumeric(Stream stream, Int32 len) { + Int64 l = 0; + Int32 r = stream.ReadByte(); + + if(r < 0) { + throw new EndOfStreamException("LBER: NUMERIC: decode error: EOF"); + } + + if((r & 0x80) != 0) { + // check for negative number + l = -1; + } + +#pragma warning disable CS0675 // Bitweiser OR-Operator, der bei einem signaturerweiterten Operanden verwendet wurde. + l = (l << 8) | r; +#pragma warning restore CS0675 // Bitweiser OR-Operator, der bei einem signaturerweiterten Operanden verwendet wurde. + + for(Int32 i = 1; i < len; i++) { + r = stream.ReadByte(); + if(r < 0) { + throw new EndOfStreamException("LBER: NUMERIC: decode error: EOF"); + } + +#pragma warning disable CS0675 // Bitweiser OR-Operator, der bei einem signaturerweiterten Operanden verwendet wurde. + l = (l << 8) | r; +#pragma warning restore CS0675 // Bitweiser OR-Operator, der bei einem signaturerweiterten Operanden verwendet wurde. + } + + return l; + } + + /// + /// Decode an OctetString directly from a stream. + /// + /// The stream. + /// Length in bytes. + /// Decoded octet. + public static Object DecodeOctetString(Stream stream, Int32 len) { + SByte[] octets = new SByte[len]; + Int32 totalLen = 0; + + while(totalLen < len) { + // Make sure we have read all the data + totalLen += stream.ReadInput(ref octets, totalLen, len - totalLen); + } + + return octets; + } + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/Ldap/LberEncoder.cs b/Unosquare.Swan/Networking/Ldap/LberEncoder.cs index eb36934..09e8351 100644 --- a/Unosquare.Swan/Networking/Ldap/LberEncoder.cs +++ b/Unosquare.Swan/Networking/Ldap/LberEncoder.cs @@ -1,246 +1,223 @@ -namespace Unosquare.Swan.Networking.Ldap -{ - using System.IO; - +using System; +using System.IO; + +namespace Unosquare.Swan.Networking.Ldap { + /// + /// This class provides LBER encoding routines for ASN.1 Types. LBER is a + /// subset of BER as described in the following taken from 5.1 of RFC 2251: + /// 5.1. Mapping Onto BER-based Transport Services + /// The protocol elements of Ldap are encoded for exchange using the + /// Basic Encoding Rules (BER) [11] of ASN.1 [3]. However, due to the + /// high overhead involved in using certain elements of the BER, the + /// following additional restrictions are placed on BER-encodings of Ldap + /// protocol elements: + ///
  • (1) Only the definite form of length encoding will be used.
  • + ///
  • (2) OCTET STRING values will be encoded in the primitive form only.
  • + /// (3) If the value of a BOOLEAN type is true, the encoding MUST have + /// its contents octets set to hex "FF". + ///
  • + /// (4) If a value of a type is its default value, it MUST be absent. + /// Only some BOOLEAN and INTEGER types have default values in this + /// protocol definition. + /// These restrictions do not apply to ASN.1 types encapsulated inside of + /// OCTET STRING values, such as attribute values, unless otherwise + /// noted. + ///
  • + /// [3] ITU-T Rec. X.680, "Abstract Syntax Notation One (ASN.1) - + /// Specification of Basic Notation", 1994. + /// [11] ITU-T Rec. X.690, "Specification of ASN.1 encoding rules: Basic, + /// Canonical, and Distinguished Encoding Rules", 1994. + ///
    + internal static class LberEncoder { /// - /// This class provides LBER encoding routines for ASN.1 Types. LBER is a - /// subset of BER as described in the following taken from 5.1 of RFC 2251: - /// 5.1. Mapping Onto BER-based Transport Services - /// The protocol elements of Ldap are encoded for exchange using the - /// Basic Encoding Rules (BER) [11] of ASN.1 [3]. However, due to the - /// high overhead involved in using certain elements of the BER, the - /// following additional restrictions are placed on BER-encodings of Ldap - /// protocol elements: - ///
  • (1) Only the definite form of length encoding will be used.
  • - ///
  • (2) OCTET STRING values will be encoded in the primitive form only.
  • - /// (3) If the value of a BOOLEAN type is true, the encoding MUST have - /// its contents octets set to hex "FF". - ///
  • - /// (4) If a value of a type is its default value, it MUST be absent. - /// Only some BOOLEAN and INTEGER types have default values in this - /// protocol definition. - /// These restrictions do not apply to ASN.1 types encapsulated inside of - /// OCTET STRING values, such as attribute values, unless otherwise - /// noted. - ///
  • - /// [3] ITU-T Rec. X.680, "Abstract Syntax Notation One (ASN.1) - - /// Specification of Basic Notation", 1994. - /// [11] ITU-T Rec. X.690, "Specification of ASN.1 encoding rules: Basic, - /// Canonical, and Distinguished Encoding Rules", 1994. + /// BER Encode an Asn1Boolean directly into the specified output stream. ///
    - internal static class LberEncoder - { - /// - /// BER Encode an Asn1Boolean directly into the specified output stream. - /// - /// The Asn1Boolean object to encode. - /// The stream. - public static void Encode(Asn1Boolean b, Stream stream) - { - Encode(b.GetIdentifier(), stream); - stream.WriteByte(0x01); - stream.WriteByte((byte) (b.BooleanValue() ? 0xff : 0x00)); - } - - /// - /// Encode an Asn1Numeric directly into the specified outputstream. - /// Use a two's complement representation in the fewest number of octets - /// possible. - /// Can be used to encode INTEGER and ENUMERATED values. - /// - /// The Asn1Numeric object to encode. - /// The stream. - public static void Encode(Asn1Numeric n, Stream stream) - { - var octets = new sbyte[8]; - sbyte len; - var longValue = n.LongValue(); - long endValue = longValue < 0 ? -1 : 0; - var endSign = endValue & 0x80; - - for (len = 0; len == 0 || longValue != endValue || (octets[len - 1] & 0x80) != endSign; len++) - { - octets[len] = (sbyte)(longValue & 0xFF); - longValue >>= 8; - } - - Encode(n.GetIdentifier(), stream); - stream.WriteByte((byte)len); - - for (var i = len - 1; i >= 0; i--) - { - stream.WriteByte((byte) octets[i]); - } - } - - /// - /// Encode an Asn1OctetString directly into the specified outputstream. - /// - /// The Asn1OctetString object to encode. - /// The stream. - public static void Encode(Asn1OctetString os, Stream stream) - { - Encode(os.GetIdentifier(), stream); - EncodeLength(os.ByteValue().Length, stream); - var tempSbyteArray = os.ByteValue(); - stream.Write(tempSbyteArray.ToByteArray(), 0, tempSbyteArray.Length); - } - - public static void Encode(Asn1Object obj, Stream stream) - { - switch (obj) - { - case Asn1Boolean b: - Encode(b, stream); - break; - case Asn1Numeric n: - Encode(n, stream); - break; - case Asn1Null n: - Encode(n.GetIdentifier(), stream); - stream.WriteByte(0x00); // Length (with no Content) - break; - case Asn1OctetString n: - Encode(n, stream); - break; - case Asn1Structured n: - Encode(n, stream); - break; - case Asn1Tagged n: - Encode(n, stream); - break; - case Asn1Choice n: - Encode(n.ChoiceValue, stream); - break; - default: - throw new InvalidDataException(); - } - } - - /// - /// Encode an Asn1Structured into the specified outputstream. This method - /// can be used to encode SET, SET_OF, SEQUENCE, SEQUENCE_OF. - /// - /// The Asn1Structured object to encode. - /// The stream. - public static void Encode(Asn1Structured c, Stream stream) - { - Encode(c.GetIdentifier(), stream); - - var arrayValue = c.ToArray(); - - using (var output = new MemoryStream()) - { - foreach (var obj in arrayValue) - { - Encode(obj, output); - } - - EncodeLength((int) output.Length, stream); - - var tempSbyteArray = output.ToArray(); - stream.Write(tempSbyteArray, 0, tempSbyteArray.Length); - } - } - - /// - /// Encode an Asn1Tagged directly into the specified outputstream. - /// - /// The Asn1Tagged object to encode. - /// The stream. - public static void Encode(Asn1Tagged t, Stream stream) - { - if (!t.Explicit) - { - Encode(t.TaggedValue, stream); - return; - } - - Encode(t.GetIdentifier(), stream); - - // determine the encoded length of the base type. - using (var encodedContent = new MemoryStream()) - { - Encode(t.TaggedValue, encodedContent); - - EncodeLength((int) encodedContent.Length, stream); - var tempSbyteArray = encodedContent.ToArray().ToSByteArray(); - stream.Write(tempSbyteArray.ToByteArray(), 0, tempSbyteArray.Length); - } - } - - /// - /// Encode an Asn1Identifier directly into the specified outputstream. - /// - /// The Asn1Identifier object to encode. - /// The stream. - public static void Encode(Asn1Identifier id, Stream stream) - { - var c = (int) id.Asn1Class; - var t = id.Tag; - var ccf = (sbyte)((c << 6) | (id.Constructed ? 0x20 : 0)); - - if (t < 30) - { - stream.WriteByte((byte)(ccf | t)); - } - else - { - stream.WriteByte((byte)(ccf | 0x1F)); - EncodeTagInteger(t, stream); - } - } - - /// - /// Encodes the length. - /// - /// The length. - /// The stream. - private static void EncodeLength(int length, Stream stream) - { - if (length < 0x80) - { - stream.WriteByte((byte)length); - } - else - { - var octets = new sbyte[4]; // 4 bytes sufficient for 32 bit int. - sbyte n; - for (n = 0; length != 0; n++) - { - octets[n] = (sbyte)(length & 0xFF); - length >>= 8; - } - - stream.WriteByte((byte)(0x80 | n)); - - for (var i = n - 1; i >= 0; i--) - stream.WriteByte((byte)octets[i]); - } - } - - /// - /// Encodes the provided tag into the stream. - /// - /// The value. - /// The stream. - private static void EncodeTagInteger(int val, Stream stream) - { - var octets = new sbyte[5]; - int n; - - for (n = 0; val != 0; n++) - { - octets[n] = (sbyte)(val & 0x7F); - val = val >> 7; - } - - for (var i = n - 1; i > 0; i--) - { - stream.WriteByte((byte)(octets[i] | 0x80)); - } - - stream.WriteByte((byte)octets[0]); - } - } + /// The Asn1Boolean object to encode. + /// The stream. + public static void Encode(Asn1Boolean b, Stream stream) { + Encode(b.GetIdentifier(), stream); + stream.WriteByte(0x01); + stream.WriteByte((Byte)(b.BooleanValue() ? 0xff : 0x00)); + } + + /// + /// Encode an Asn1Numeric directly into the specified outputstream. + /// Use a two's complement representation in the fewest number of octets + /// possible. + /// Can be used to encode INTEGER and ENUMERATED values. + /// + /// The Asn1Numeric object to encode. + /// The stream. + public static void Encode(Asn1Numeric n, Stream stream) { + SByte[] octets = new SByte[8]; + SByte len; + Int64 longValue = n.LongValue(); + Int64 endValue = longValue < 0 ? -1 : 0; + Int64 endSign = endValue & 0x80; + + for(len = 0; len == 0 || longValue != endValue || (octets[len - 1] & 0x80) != endSign; len++) { + octets[len] = (SByte)(longValue & 0xFF); + longValue >>= 8; + } + + Encode(n.GetIdentifier(), stream); + stream.WriteByte((Byte)len); + + for(Int32 i = len - 1; i >= 0; i--) { + stream.WriteByte((Byte)octets[i]); + } + } + + /// + /// Encode an Asn1OctetString directly into the specified outputstream. + /// + /// The Asn1OctetString object to encode. + /// The stream. + public static void Encode(Asn1OctetString os, Stream stream) { + Encode(os.GetIdentifier(), stream); + EncodeLength(os.ByteValue().Length, stream); + SByte[] tempSbyteArray = os.ByteValue(); + stream.Write(tempSbyteArray.ToByteArray(), 0, tempSbyteArray.Length); + } + + public static void Encode(Asn1Object obj, Stream stream) { + switch(obj) { + case Asn1Boolean b: + Encode(b, stream); + break; + case Asn1Numeric n: + Encode(n, stream); + break; + case Asn1Null n: + Encode(n.GetIdentifier(), stream); + stream.WriteByte(0x00); // Length (with no Content) + break; + case Asn1OctetString n: + Encode(n, stream); + break; + case Asn1Structured n: + Encode(n, stream); + break; + case Asn1Tagged n: + Encode(n, stream); + break; + case Asn1Choice n: + Encode(n.ChoiceValue, stream); + break; + default: + throw new InvalidDataException(); + } + } + + /// + /// Encode an Asn1Structured into the specified outputstream. This method + /// can be used to encode SET, SET_OF, SEQUENCE, SEQUENCE_OF. + /// + /// The Asn1Structured object to encode. + /// The stream. + public static void Encode(Asn1Structured c, Stream stream) { + Encode(c.GetIdentifier(), stream); + + Asn1Object[] arrayValue = c.ToArray(); + + using(MemoryStream output = new MemoryStream()) { + foreach(Asn1Object obj in arrayValue) { + Encode(obj, output); + } + + EncodeLength((Int32)output.Length, stream); + + Byte[] tempSbyteArray = output.ToArray(); + stream.Write(tempSbyteArray, 0, tempSbyteArray.Length); + } + } + + /// + /// Encode an Asn1Tagged directly into the specified outputstream. + /// + /// The Asn1Tagged object to encode. + /// The stream. + public static void Encode(Asn1Tagged t, Stream stream) { + if(!t.Explicit) { + Encode(t.TaggedValue, stream); + return; + } + + Encode(t.GetIdentifier(), stream); + + // determine the encoded length of the base type. + using(MemoryStream encodedContent = new MemoryStream()) { + Encode(t.TaggedValue, encodedContent); + + EncodeLength((Int32)encodedContent.Length, stream); + SByte[] tempSbyteArray = encodedContent.ToArray().ToSByteArray(); + stream.Write(tempSbyteArray.ToByteArray(), 0, tempSbyteArray.Length); + } + } + + /// + /// Encode an Asn1Identifier directly into the specified outputstream. + /// + /// The Asn1Identifier object to encode. + /// The stream. + public static void Encode(Asn1Identifier id, Stream stream) { + Int32 c = (Int32)id.Asn1Class; + Int32 t = id.Tag; + SByte ccf = (SByte)((c << 6) | (id.Constructed ? 0x20 : 0)); + + if(t < 30) { +#pragma warning disable CS0675 // Bitweiser OR-Operator, der bei einem signaturerweiterten Operanden verwendet wurde. + stream.WriteByte((Byte)(ccf | t)); +#pragma warning restore CS0675 // Bitweiser OR-Operator, der bei einem signaturerweiterten Operanden verwendet wurde. + } else { + stream.WriteByte((Byte)(ccf | 0x1F)); + EncodeTagInteger(t, stream); + } + } + + /// + /// Encodes the length. + /// + /// The length. + /// The stream. + private static void EncodeLength(Int32 length, Stream stream) { + if(length < 0x80) { + stream.WriteByte((Byte)length); + } else { + SByte[] octets = new SByte[4]; // 4 bytes sufficient for 32 bit int. + SByte n; + for(n = 0; length != 0; n++) { + octets[n] = (SByte)(length & 0xFF); + length >>= 8; + } + + stream.WriteByte((Byte)(0x80 | n)); + + for(Int32 i = n - 1; i >= 0; i--) { + stream.WriteByte((Byte)octets[i]); + } + } + } + + /// + /// Encodes the provided tag into the stream. + /// + /// The value. + /// The stream. + private static void EncodeTagInteger(Int32 val, Stream stream) { + SByte[] octets = new SByte[5]; + Int32 n; + + for(n = 0; val != 0; n++) { + octets[n] = (SByte)(val & 0x7F); + val >>= 7; + } + + for(Int32 i = n - 1; i > 0; i--) { + stream.WriteByte((Byte)(octets[i] | 0x80)); + } + + stream.WriteByte((Byte)octets[0]); + } + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/Ldap/LdapConnection.cs b/Unosquare.Swan/Networking/Ldap/LdapConnection.cs index bbba3e5..cda85f9 100644 --- a/Unosquare.Swan/Networking/Ldap/LdapConnection.cs +++ b/Unosquare.Swan/Networking/Ldap/LdapConnection.cs @@ -1,402 +1,382 @@ -namespace Unosquare.Swan.Networking.Ldap -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.IO; - using System.Net.Sockets; - using System.Text; - using System.Threading; - using System.Threading.Tasks; - using Exceptions; - +using System; +using System.Collections.Generic; +using System.Linq; +using System.IO; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Unosquare.Swan.Exceptions; + +namespace Unosquare.Swan.Networking.Ldap { + /// + /// The central class that encapsulates the connection + /// to a directory server through the Ldap protocol. + /// LdapConnection objects are used to perform common Ldap + /// operations such as search, modify and add. + /// In addition, LdapConnection objects allow you to bind to an + /// Ldap server, set connection and search constraints, and perform + /// several other tasks. + /// An LdapConnection object is not connected on + /// construction and can only be connected to one server at one + /// port. + /// + /// Based on https://github.com/dsbenghe/Novell.Directory.Ldap.NETStandard. + /// + /// + /// The following code describes how to use the LdapConnection class: + /// + /// + /// class Example + /// { + /// using Unosquare.Swan; + /// using Unosquare.Swan.Networking.Ldap; + /// using System.Threading.Tasks; + /// + /// static async Task Main() + /// { + /// // create a LdapConnection object + /// var connection = new LdapConnection(); + /// + /// // connect to a server + /// await connection.Connect("ldap.forumsys.com", 389); + /// + /// // set up the credentials + /// await connection.Bind("cn=read-only-admin,dc=example,dc=com", "password"); + /// + /// // retrieve all entries that have the specified email using ScopeSub + /// // which searches all entries at all levels under + /// // and including the specified base DN + /// var searchResult = await connection + /// .Search("dc=example,dc=com", LdapConnection.ScopeSub, "(cn=Isaac Newton)"); + /// + /// // if there are more entries remaining keep going + /// while (searchResult.HasMore()) + /// { + /// // point to the next entry + /// var entry = searchResult.Next(); + /// + /// // get all attributes + /// var entryAttributes = entry.GetAttributeSet(); + /// + /// // select its name and print it out + /// entryAttributes.GetAttribute("cn").StringValue.Info(); + /// } + /// + /// // modify Tesla and sets its email as tesla@email.com + /// connection.Modify("uid=tesla,dc=example,dc=com", + /// new[] { + /// new LdapModification(LdapModificationOp.Replace, + /// "mail", "tesla@email.com") + /// }); + /// + /// // delete the listed values from the given attribute + /// connection.Modify("uid=tesla,dc=example,dc=com", + /// new[] { + /// new LdapModification(LdapModificationOp.Delete, + /// "mail", "tesla@email.com") + /// }); + /// + /// // add back the recently deleted property + /// connection.Modify("uid=tesla,dc=example,dc=com", + /// new[] { + /// new LdapModification(LdapModificationOp.Add, + /// "mail", "tesla@email.com") + /// }); + /// + /// // disconnect from the LDAP server + /// connection.Disconnect(); + /// + /// Terminal.Flush(); + /// } + /// } + /// + /// + public class LdapConnection : IDisposable { + private const Int32 LdapV3 = 3; + + private readonly CancellationTokenSource _cts = new CancellationTokenSource(); + [System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0069:Verwerfbare Felder verwerfen", Justification = "")] + private Connection _conn; + private Boolean _isDisposing; + /// - /// The central class that encapsulates the connection - /// to a directory server through the Ldap protocol. - /// LdapConnection objects are used to perform common Ldap - /// operations such as search, modify and add. - /// In addition, LdapConnection objects allow you to bind to an - /// Ldap server, set connection and search constraints, and perform - /// several other tasks. - /// An LdapConnection object is not connected on - /// construction and can only be connected to one server at one - /// port. - /// - /// Based on https://github.com/dsbenghe/Novell.Directory.Ldap.NETStandard. + /// Returns the protocol version uses to authenticate. + /// 0 is returned if no authentication has been performed. /// - /// - /// The following code describes how to use the LdapConnection class: - /// - /// - /// class Example - /// { - /// using Unosquare.Swan; - /// using Unosquare.Swan.Networking.Ldap; - /// using System.Threading.Tasks; - /// - /// static async Task Main() - /// { - /// // create a LdapConnection object - /// var connection = new LdapConnection(); - /// - /// // connect to a server - /// await connection.Connect("ldap.forumsys.com", 389); - /// - /// // set up the credentials - /// await connection.Bind("cn=read-only-admin,dc=example,dc=com", "password"); - /// - /// // retrieve all entries that have the specified email using ScopeSub - /// // which searches all entries at all levels under - /// // and including the specified base DN - /// var searchResult = await connection - /// .Search("dc=example,dc=com", LdapConnection.ScopeSub, "(cn=Isaac Newton)"); - /// - /// // if there are more entries remaining keep going - /// while (searchResult.HasMore()) - /// { - /// // point to the next entry - /// var entry = searchResult.Next(); - /// - /// // get all attributes - /// var entryAttributes = entry.GetAttributeSet(); - /// - /// // select its name and print it out - /// entryAttributes.GetAttribute("cn").StringValue.Info(); - /// } - /// - /// // modify Tesla and sets its email as tesla@email.com - /// connection.Modify("uid=tesla,dc=example,dc=com", - /// new[] { - /// new LdapModification(LdapModificationOp.Replace, - /// "mail", "tesla@email.com") - /// }); - /// - /// // delete the listed values from the given attribute - /// connection.Modify("uid=tesla,dc=example,dc=com", - /// new[] { - /// new LdapModification(LdapModificationOp.Delete, - /// "mail", "tesla@email.com") - /// }); - /// - /// // add back the recently deleted property - /// connection.Modify("uid=tesla,dc=example,dc=com", - /// new[] { - /// new LdapModification(LdapModificationOp.Add, - /// "mail", "tesla@email.com") - /// }); - /// - /// // disconnect from the LDAP server - /// connection.Disconnect(); - /// - /// Terminal.Flush(); - /// } - /// } - /// - /// - public class LdapConnection : IDisposable - { - private const int LdapV3 = 3; - - private readonly CancellationTokenSource _cts = new CancellationTokenSource(); - - private Connection _conn; - private bool _isDisposing; - - /// - /// Returns the protocol version uses to authenticate. - /// 0 is returned if no authentication has been performed. - /// - /// - /// The protocol version. - /// - public int ProtocolVersion => BindProperties?.ProtocolVersion ?? LdapV3; - - /// - /// Returns the distinguished name (DN) used for as the bind name during - /// the last successful bind operation. null is returned - /// if no authentication has been performed or if the bind resulted in - /// an anonymous connection. - /// - /// - /// The authentication dn. - /// - public string AuthenticationDn => BindProperties == null ? null : (BindProperties.Anonymous ? null : BindProperties.AuthenticationDN); - - /// - /// Returns the method used to authenticate the connection. The return - /// value is one of the following:. - ///
    • "none" indicates the connection is not authenticated.
    • - /// "simple" indicates simple authentication was used or that a null - /// or empty authentication DN was specified. - ///
    • "sasl" indicates that a SASL mechanism was used to authenticate
    - ///
    - /// - /// The authentication method. - /// - public string AuthenticationMethod => BindProperties == null ? "simple" : BindProperties.AuthenticationMethod; - - /// - /// Indicates whether the connection represented by this object is open - /// at this time. - /// - /// - /// True if connection is open; false if the connection is closed. - /// - public bool Connected => _conn?.IsConnected == true; - - internal BindProperties BindProperties { get; set; } - - internal List Messages { get; } = new List(); - - /// - public void Dispose() - { - if (_isDisposing) return; - - _isDisposing = true; - Disconnect(); - _cts?.Dispose(); - } - - /// - /// Synchronously authenticates to the Ldap server (that the object is - /// currently connected to) using the specified name, password, Ldap version, - /// and constraints. - /// If the object has been disconnected from an Ldap server, - /// this method attempts to reconnect to the server. If the object - /// has already authenticated, the old authentication is discarded. - /// - /// If non-null and non-empty, specifies that the - /// connection and all operations through it should - /// be authenticated with dn as the distinguished - /// name. - /// If non-null and non-empty, specifies that the - /// connection and all operations through it should - /// be authenticated with dn as the distinguished - /// name and password. - /// Note: the application should use care in the use - /// of String password objects. These are long lived - /// objects, and may expose a security risk, especially - /// in objects that are serialized. The LdapConnection - /// keeps no long lived instances of these objects. - /// - /// A representing the asynchronous operation. - /// - public Task Bind(string dn, string password) => Bind(LdapV3, dn, password); - - /// - /// Synchronously authenticates to the Ldap server (that the object is - /// currently connected to) using the specified name, password, Ldap version, - /// and constraints. - /// If the object has been disconnected from an Ldap server, - /// this method attempts to reconnect to the server. If the object - /// has already authenticated, the old authentication is discarded. - /// - /// The Ldap protocol version, use Ldap_V3. - /// Ldap_V2 is not supported. - /// If non-null and non-empty, specifies that the - /// connection and all operations through it should - /// be authenticated with dn as the distinguished - /// name. - /// If non-null and non-empty, specifies that the - /// connection and all operations through it should - /// be authenticated with dn as the distinguished - /// name and passwd as password. - /// Note: the application should use care in the use - /// of String password objects. These are long lived - /// objects, and may expose a security risk, especially - /// in objects that are serialized. The LdapConnection - /// keeps no long lived instances of these objects. - /// A representing the asynchronous operation. - public Task Bind(int version, string dn, string password) - { - dn = string.IsNullOrEmpty(dn) ? string.Empty : dn.Trim(); - var passwordData = string.IsNullOrWhiteSpace(password) ? new sbyte[] { } : Encoding.UTF8.GetSBytes(password); - - var anonymous = false; - - if (passwordData.Length == 0) - { - anonymous = true; // anonymous, password length zero with simple bind - dn = string.Empty; // set to null if anonymous - } - - BindProperties = new BindProperties(version, dn, "simple", anonymous); - - return RequestLdapMessage(new LdapBindRequest(version, dn, passwordData)); - } - - /// - /// Connects to the specified host and port. - /// If this LdapConnection object represents an open connection, the - /// connection is closed first before the new connection is opened. - /// At this point, there is no authentication, and any operations are - /// conducted as an anonymous client. - /// - /// A host name or a dotted string representing the IP address - /// of a host running an Ldap server. - /// The TCP or UDP port number to connect to or contact. - /// The default Ldap port is 389. - /// A representing the asynchronous operation. - public async Task Connect(string host, int port) - { - var tcpClient = new TcpClient(); - await tcpClient.ConnectAsync(host, port).ConfigureAwait(false); - _conn = new Connection(tcpClient, Encoding.UTF8, "\r\n", true, 0); - + /// + /// The protocol version. + /// + public Int32 ProtocolVersion => this.BindProperties?.ProtocolVersion ?? LdapV3; + + /// + /// Returns the distinguished name (DN) used for as the bind name during + /// the last successful bind operation. null is returned + /// if no authentication has been performed or if the bind resulted in + /// an anonymous connection. + /// + /// + /// The authentication dn. + /// + public String AuthenticationDn => this.BindProperties == null ? null : (this.BindProperties.Anonymous ? null : this.BindProperties.AuthenticationDN); + + /// + /// Returns the method used to authenticate the connection. The return + /// value is one of the following:. + ///
    • "none" indicates the connection is not authenticated.
    • + /// "simple" indicates simple authentication was used or that a null + /// or empty authentication DN was specified. + ///
    • "sasl" indicates that a SASL mechanism was used to authenticate
    + ///
    + /// + /// The authentication method. + /// + public String AuthenticationMethod => this.BindProperties == null ? "simple" : this.BindProperties.AuthenticationMethod; + + /// + /// Indicates whether the connection represented by this object is open + /// at this time. + /// + /// + /// True if connection is open; false if the connection is closed. + /// + public Boolean Connected => this._conn?.IsConnected == true; + + internal BindProperties BindProperties { + get; set; + } + + internal List Messages { get; } = new List(); + + /// + public void Dispose() { + if(this._isDisposing) { + return; + } + + this._isDisposing = true; + this.Disconnect(); + this._cts?.Dispose(); + } + + /// + /// Synchronously authenticates to the Ldap server (that the object is + /// currently connected to) using the specified name, password, Ldap version, + /// and constraints. + /// If the object has been disconnected from an Ldap server, + /// this method attempts to reconnect to the server. If the object + /// has already authenticated, the old authentication is discarded. + /// + /// If non-null and non-empty, specifies that the + /// connection and all operations through it should + /// be authenticated with dn as the distinguished + /// name. + /// If non-null and non-empty, specifies that the + /// connection and all operations through it should + /// be authenticated with dn as the distinguished + /// name and password. + /// Note: the application should use care in the use + /// of String password objects. These are long lived + /// objects, and may expose a security risk, especially + /// in objects that are serialized. The LdapConnection + /// keeps no long lived instances of these objects. + /// + /// A representing the asynchronous operation. + /// + public Task Bind(String dn, String password) => this.Bind(LdapV3, dn, password); + + /// + /// Synchronously authenticates to the Ldap server (that the object is + /// currently connected to) using the specified name, password, Ldap version, + /// and constraints. + /// If the object has been disconnected from an Ldap server, + /// this method attempts to reconnect to the server. If the object + /// has already authenticated, the old authentication is discarded. + /// + /// The Ldap protocol version, use Ldap_V3. + /// Ldap_V2 is not supported. + /// If non-null and non-empty, specifies that the + /// connection and all operations through it should + /// be authenticated with dn as the distinguished + /// name. + /// If non-null and non-empty, specifies that the + /// connection and all operations through it should + /// be authenticated with dn as the distinguished + /// name and passwd as password. + /// Note: the application should use care in the use + /// of String password objects. These are long lived + /// objects, and may expose a security risk, especially + /// in objects that are serialized. The LdapConnection + /// keeps no long lived instances of these objects. + /// A representing the asynchronous operation. + public Task Bind(Int32 version, String dn, String password) { + dn = String.IsNullOrEmpty(dn) ? String.Empty : dn.Trim(); + SByte[] passwordData = String.IsNullOrWhiteSpace(password) ? new SByte[] { } : Encoding.UTF8.GetSBytes(password); + + Boolean anonymous = false; + + if(passwordData.Length == 0) { + anonymous = true; // anonymous, password length zero with simple bind + dn = String.Empty; // set to null if anonymous + } + + this.BindProperties = new BindProperties(version, dn, "simple", anonymous); + + return this.RequestLdapMessage(new LdapBindRequest(version, dn, passwordData)); + } + + /// + /// Connects to the specified host and port. + /// If this LdapConnection object represents an open connection, the + /// connection is closed first before the new connection is opened. + /// At this point, there is no authentication, and any operations are + /// conducted as an anonymous client. + /// + /// A host name or a dotted string representing the IP address + /// of a host running an Ldap server. + /// The TCP or UDP port number to connect to or contact. + /// The default Ldap port is 389. + /// A representing the asynchronous operation. + public async Task Connect(String host, Int32 port) { + TcpClient tcpClient = new TcpClient(); + await tcpClient.ConnectAsync(host, port).ConfigureAwait(false); + this._conn = new Connection(tcpClient, Encoding.UTF8, "\r\n", true, 0); + #pragma warning disable 4014 - Task.Run(() => RetrieveMessages(), _cts.Token); + _ = Task.Run(() => this.RetrieveMessages(), this._cts.Token); #pragma warning restore 4014 - } - - /// - /// Synchronously disconnects from the Ldap server. - /// Before the object can perform Ldap operations again, it must - /// reconnect to the server by calling connect. - /// The disconnect method abandons any outstanding requests, issues an - /// unbind request to the server, and then closes the socket. - /// - public void Disconnect() - { - // disconnect from API call - _cts.Cancel(); - _conn.Disconnect(); - } - - /// - /// Synchronously reads the entry for the specified distinguished name (DN), - /// using the specified constraints, and retrieves only the specified - /// attributes from the entry. - /// - /// The distinguished name of the entry to retrieve. - /// The names of the attributes to retrieve. - /// The cancellation token. - /// - /// the LdapEntry read from the server. - /// - /// Read response is ambiguous, multiple entries returned. - public async Task Read(string dn, string[] attrs = null, CancellationToken ct = default) - { - var sr = await Search(dn, LdapScope.ScopeSub, null, attrs, false, ct); - LdapEntry ret = null; - - if (sr.HasMore()) - { - ret = sr.Next(); - if (sr.HasMore()) - { - throw new LdapException("Read response is ambiguous, multiple entries returned", LdapStatusCode.AmbiguousResponse); - } - } - - return ret; - } - - /// - /// Performs the search specified by the parameters, - /// also allowing specification of constraints for the search (such - /// as the maximum number of entries to find or the maximum time to - /// wait for search results). - /// - /// The base distinguished name to search from. - /// The scope of the entries to search. - /// The search filter specifying the search criteria. - /// The names of attributes to retrieve. - /// If true, returns the names but not the values of - /// the attributes found. If false, returns the - /// names and values for attributes found. - /// The cancellation token. - /// - /// A representing the asynchronous operation. - /// - public async Task Search( - string @base, - LdapScope scope, - string filter = "objectClass=*", - string[] attrs = null, - bool typesOnly = false, - CancellationToken ct = default) - { - // TODO: Add Search options - var msg = new LdapSearchRequest(@base, scope, filter, attrs, 0, 1000, 0, typesOnly, null); - - await RequestLdapMessage(msg, ct).ConfigureAwait(false); - - return new LdapSearchResults(Messages, msg.MessageId); - } - - /// - /// Modifies the specified dn. - /// - /// Name of the distinguished. - /// The mods. - /// The cancellation token. - /// - /// A representing the asynchronous operation. - /// - /// distinguishedName. - public Task Modify(string distinguishedName, LdapModification[] mods, CancellationToken ct = default) - { - if (distinguishedName == null) - { - throw new ArgumentNullException(nameof(distinguishedName)); - } - - return RequestLdapMessage(new LdapModifyRequest(distinguishedName, mods, null), ct); - } - - internal async Task RequestLdapMessage(LdapMessage msg, CancellationToken ct = default) - { - using (var stream = new MemoryStream()) - { - LberEncoder.Encode(msg.Asn1Object, stream); - await _conn.WriteDataAsync(stream.ToArray(), true, ct).ConfigureAwait(false); - - try - { - while (new List(Messages).Any(x => x.MessageId == msg.MessageId) == false) - await Task.Delay(100, ct).ConfigureAwait(false); - } - catch (ArgumentException) - { - // expected - } - - var first = new List(Messages).FirstOrDefault(x => x.MessageId == msg.MessageId); - - if (first != null) - { - var response = new LdapResponse(first); - response.ChkResultCode(); - } - } - } - - internal void RetrieveMessages() - { - while (!_cts.IsCancellationRequested) - { - try - { - var asn1Id = new Asn1Identifier(_conn.ActiveStream); - - if (asn1Id.Tag != Asn1Sequence.Tag) - { - continue; // loop looking for an RfcLdapMessage identifier - } - - // Turn the message into an RfcMessage class - var asn1Len = new Asn1Length(_conn.ActiveStream); - - Messages.Add(new RfcLdapMessage(_conn.ActiveStream, asn1Len.Length)); - } - catch (IOException) - { - // ignore - } - } - - // ReSharper disable once FunctionNeverReturns - } - } + } + + /// + /// Synchronously disconnects from the Ldap server. + /// Before the object can perform Ldap operations again, it must + /// reconnect to the server by calling connect. + /// The disconnect method abandons any outstanding requests, issues an + /// unbind request to the server, and then closes the socket. + /// + public void Disconnect() { + // disconnect from API call + this._cts.Cancel(); + this._conn.Disconnect(); + } + + /// + /// Synchronously reads the entry for the specified distinguished name (DN), + /// using the specified constraints, and retrieves only the specified + /// attributes from the entry. + /// + /// The distinguished name of the entry to retrieve. + /// The names of the attributes to retrieve. + /// The cancellation token. + /// + /// the LdapEntry read from the server. + /// + /// Read response is ambiguous, multiple entries returned. + public async Task Read(String dn, String[] attrs = null, CancellationToken ct = default) { + LdapSearchResults sr = await this.Search(dn, LdapScope.ScopeSub, null, attrs, false, ct); + LdapEntry ret = null; + + if(sr.HasMore()) { + ret = sr.Next(); + if(sr.HasMore()) { + throw new LdapException("Read response is ambiguous, multiple entries returned", LdapStatusCode.AmbiguousResponse); + } + } + + return ret; + } + + /// + /// Performs the search specified by the parameters, + /// also allowing specification of constraints for the search (such + /// as the maximum number of entries to find or the maximum time to + /// wait for search results). + /// + /// The base distinguished name to search from. + /// The scope of the entries to search. + /// The search filter specifying the search criteria. + /// The names of attributes to retrieve. + /// If true, returns the names but not the values of + /// the attributes found. If false, returns the + /// names and values for attributes found. + /// The cancellation token. + /// + /// A representing the asynchronous operation. + /// + public async Task Search( + String @base, + LdapScope scope, + String filter = "objectClass=*", + String[] attrs = null, + Boolean typesOnly = false, + CancellationToken ct = default) { + // TODO: Add Search options + LdapSearchRequest msg = new LdapSearchRequest(@base, scope, filter, attrs, 0, 1000, 0, typesOnly, null); + + await this.RequestLdapMessage(msg, ct).ConfigureAwait(false); + + return new LdapSearchResults(this.Messages, msg.MessageId); + } + + /// + /// Modifies the specified dn. + /// + /// Name of the distinguished. + /// The mods. + /// The cancellation token. + /// + /// A representing the asynchronous operation. + /// + /// distinguishedName. + public Task Modify(String distinguishedName, LdapModification[] mods, CancellationToken ct = default) { + if(distinguishedName == null) { + throw new ArgumentNullException(nameof(distinguishedName)); + } + + return this.RequestLdapMessage(new LdapModifyRequest(distinguishedName, mods, null), ct); + } + + internal async Task RequestLdapMessage(LdapMessage msg, CancellationToken ct = default) { + using(MemoryStream stream = new MemoryStream()) { + LberEncoder.Encode(msg.Asn1Object, stream); + await this._conn.WriteDataAsync(stream.ToArray(), true, ct).ConfigureAwait(false); + + try { + while(new List(this.Messages).Any(x => x.MessageId == msg.MessageId) == false) { + await Task.Delay(100, ct).ConfigureAwait(false); + } + } catch(ArgumentException) { + // expected + } + + RfcLdapMessage first = new List(this.Messages).FirstOrDefault(x => x.MessageId == msg.MessageId); + + if(first != null) { + LdapResponse response = new LdapResponse(first); + response.ChkResultCode(); + } + } + } + + internal void RetrieveMessages() { + while(!this._cts.IsCancellationRequested) { + try { + Asn1Identifier asn1Id = new Asn1Identifier(this._conn.ActiveStream); + + if(asn1Id.Tag != Asn1Sequence.Tag) { + continue; // loop looking for an RfcLdapMessage identifier + } + + // Turn the message into an RfcMessage class + Asn1Length asn1Len = new Asn1Length(this._conn.ActiveStream); + + this.Messages.Add(new RfcLdapMessage(this._conn.ActiveStream, asn1Len.Length)); + } catch(IOException) { + // ignore + } + } + + // ReSharper disable once FunctionNeverReturns + } + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/Ldap/LdapControl.cs b/Unosquare.Swan/Networking/Ldap/LdapControl.cs index 98e7faa..8569753 100644 --- a/Unosquare.Swan/Networking/Ldap/LdapControl.cs +++ b/Unosquare.Swan/Networking/Ldap/LdapControl.cs @@ -1,292 +1,275 @@ -namespace Unosquare.Swan.Networking.Ldap -{ - using System; - using System.Collections.Generic; - using Exceptions; - +using System; +using System.Collections.Generic; +using Unosquare.Swan.Exceptions; + +namespace Unosquare.Swan.Networking.Ldap { + /// + /// Encapsulates optional additional parameters or constraints to be applied to + /// an Ldap operation. + /// When included with LdapConstraints or LdapSearchConstraints + /// on an LdapConnection or with a specific operation request, it is + /// sent to the server along with operation requests. + /// + public class LdapControl { /// - /// Encapsulates optional additional parameters or constraints to be applied to - /// an Ldap operation. - /// When included with LdapConstraints or LdapSearchConstraints - /// on an LdapConnection or with a specific operation request, it is - /// sent to the server along with operation requests. + /// Initializes a new instance of the class. + /// Constructs a new LdapControl object using the specified values. /// - public class LdapControl - { - /// - /// Initializes a new instance of the class. - /// Constructs a new LdapControl object using the specified values. - /// - /// The OID of the control, as a dotted string. - /// True if the Ldap operation should be discarded if - /// the control is not supported. False if - /// the operation can be processed without the control. - /// The control-specific data. - /// An OID must be specified. - public LdapControl(string oid, bool critical, sbyte[] values) - { - if (oid == null) - { - throw new ArgumentException("An OID must be specified"); - } - - Asn1Object = new RfcControl( - oid, - new Asn1Boolean(critical), - values == null ? null : new Asn1OctetString(values)); - } - - /// - /// Returns the identifier of the control. - /// - /// - /// The identifier. - /// - public string Id => Asn1Object.ControlType.StringValue(); - - /// - /// Returns whether the control is critical for the operation. - /// - /// - /// true if critical; otherwise, false. - /// - public bool Critical => Asn1Object.Criticality.BooleanValue(); - - internal static RespControlVector RegisteredControls { get; } = new RespControlVector(5); - - internal RfcControl Asn1Object { get; } - - /// - /// Registers a class to be instantiated on receipt of a control with the - /// given OID. - /// Any previous registration for the OID is overridden. The - /// controlClass must be an extension of LdapControl. - /// - /// The object identifier of the control. - /// A class which can instantiate an LdapControl. - public static void Register(string oid, Type controlClass) - => RegisteredControls.RegisterResponseControl(oid, controlClass); - - /// - /// Returns the control-specific data of the object. - /// - /// - /// The control-specific data of the object as a byte array, - /// or null if the control has no data. - /// - public sbyte[] GetValue() => Asn1Object.ControlValue?.ByteValue(); - - internal void SetValue(sbyte[] controlValue) - { - Asn1Object.ControlValue = new Asn1OctetString(controlValue); - } - } - + /// The OID of the control, as a dotted string. + /// True if the Ldap operation should be discarded if + /// the control is not supported. False if + /// the operation can be processed without the control. + /// The control-specific data. + /// An OID must be specified. + public LdapControl(String oid, Boolean critical, SByte[] values) { + if(oid == null) { + throw new ArgumentException("An OID must be specified"); + } + + this.Asn1Object = new RfcControl( + oid, + new Asn1Boolean(critical), + values == null ? null : new Asn1OctetString(values)); + } + /// - /// Represents a simple bind request. + /// Returns the identifier of the control. /// - /// - public class LdapBindRequest : LdapMessage - { - /// - /// Initializes a new instance of the class. - /// Constructs a simple bind request. - /// - /// The Ldap protocol version, use Ldap_V3. - /// Ldap_V2 is not supported. - /// If non-null and non-empty, specifies that the - /// connection and all operations through it should - /// be authenticated with dn as the distinguished - /// name. - /// If non-null and non-empty, specifies that the - /// connection and all operations through it should - /// be authenticated with dn as the distinguished - /// name and passwd as password. - public LdapBindRequest(int version, string dn, sbyte[] password) - : base(LdapOperation.BindRequest, new RfcBindRequest(version, dn, password)) - { - } - - /// - /// Retrieves the Authentication DN for a bind request. - /// - /// - /// The authentication dn. - /// - public string AuthenticationDN => Asn1Object.RequestDn; - - /// - public override string ToString() => Asn1Object.ToString(); - } - + /// + /// The identifier. + /// + public String Id => this.Asn1Object.ControlType.StringValue(); + /// - /// Encapsulates a continuation reference from an asynchronous search operation. + /// Returns whether the control is critical for the operation. /// - /// - internal class LdapSearchResultReference : LdapMessage - { - /// - /// Initializes a new instance of the class. - /// Constructs an LdapSearchResultReference object. - /// - /// The LdapMessage with a search reference. - internal LdapSearchResultReference(RfcLdapMessage message) - : base(message) - { - } - - /// - /// Returns any URLs in the object. - /// - /// - /// The referrals. - /// - public string[] Referrals - { - get - { - var references = ((RfcSearchResultReference)Message.Response).ToArray(); - var srefs = new string[references.Length]; - for (var i = 0; i < references.Length; i++) - { - srefs[i] = ((Asn1OctetString)references[i]).StringValue(); - } - - return srefs; - } - } - } - - internal class LdapResponse : LdapMessage - { - internal LdapResponse(RfcLdapMessage message) - : base(message) - { - } - - public string ErrorMessage => ((IRfcResponse)Message.Response).GetErrorMessage().StringValue(); - - public string MatchedDN => ((IRfcResponse)Message.Response).GetMatchedDN().StringValue(); - - public LdapStatusCode ResultCode => Message.Response is RfcSearchResultEntry || - (IRfcResponse)Message.Response is RfcIntermediateResponse - ? LdapStatusCode.Success - : (LdapStatusCode)((IRfcResponse)Message.Response).GetResultCode().IntValue(); - - internal LdapException Exception { get; set; } - - internal void ChkResultCode() - { - if (Exception != null) - { - throw Exception; - } - - switch (ResultCode) - { - case LdapStatusCode.Success: - case LdapStatusCode.CompareTrue: - case LdapStatusCode.CompareFalse: - break; - case LdapStatusCode.Referral: - throw new LdapException( - "Automatic referral following not enabled", - LdapStatusCode.Referral, - ErrorMessage); - default: - throw new LdapException(ResultCode.ToString().Humanize(), ResultCode, ErrorMessage, MatchedDN); - } - } - } - + /// + /// true if critical; otherwise, false. + /// + public Boolean Critical => this.Asn1Object.Criticality.BooleanValue(); + + internal static RespControlVector RegisteredControls { get; } = new RespControlVector(5); + + internal RfcControl Asn1Object { + get; + } + /// - /// The RespControlVector class implements extends the - /// existing Vector class so that it can be used to maintain a - /// list of currently registered control responses. + /// Registers a class to be instantiated on receipt of a control with the + /// given OID. + /// Any previous registration for the OID is overridden. The + /// controlClass must be an extension of LdapControl. /// - internal class RespControlVector : List - { - private readonly object _syncLock = new object(); - - public RespControlVector(int cap) - : base(cap) - { - } - - public void RegisterResponseControl(string oid, Type controlClass) - { - lock (_syncLock) - { - Add(new RegisteredControl(this, oid, controlClass)); - } - } - - /// - /// Inner class defined to create a temporary object to encapsulate - /// all registration information about a response control. - /// - internal class RegisteredControl - { - public RegisteredControl(RespControlVector enclosingInstance, string oid, Type controlClass) - { - EnclosingInstance = enclosingInstance; - MyOid = oid; - MyClass = controlClass; - } - - internal Type MyClass { get; } - - internal string MyOid { get; } - - private RespControlVector EnclosingInstance { get; } - } - } - + /// The object identifier of the control. + /// A class which can instantiate an LdapControl. + public static void Register(String oid, Type controlClass) + => RegisteredControls.RegisterResponseControl(oid, controlClass); + /// - /// Represents and Ldap Bind Request. - ///
    -    /// BindRequest ::= [APPLICATION 0] SEQUENCE {
    -    /// version                 INTEGER (1 .. 127),
    -    /// name                    LdapDN,
    -    /// authentication          AuthenticationChoice }
    -    /// 
    - /// - /// - internal sealed class RfcBindRequest - : Asn1Sequence, IRfcRequest - { - private readonly sbyte[] _password; - private static readonly Asn1Identifier Id = new Asn1Identifier(LdapOperation.BindRequest); - - public RfcBindRequest(int version, string name, sbyte[] password) - : base(3) - { - _password = password; - Add(new Asn1Integer(version)); - Add(name); - Add(new RfcAuthenticationChoice(password)); - } - - public Asn1Integer Version - { - get => (Asn1Integer)Get(0); - set => Set(0, value); - } - - public Asn1OctetString Name - { - get => (Asn1OctetString)Get(1); - set => Set(1, value); - } - - public RfcAuthenticationChoice AuthenticationChoice - { - get => (RfcAuthenticationChoice)Get(2); - set => Set(2, value); - } - - public override Asn1Identifier GetIdentifier() => Id; - - public string GetRequestDN() => ((Asn1OctetString)Get(1)).StringValue(); - } + /// Returns the control-specific data of the object. + /// + /// + /// The control-specific data of the object as a byte array, + /// or null if the control has no data. + /// + public SByte[] GetValue() => this.Asn1Object.ControlValue?.ByteValue(); + + internal void SetValue(SByte[] controlValue) => this.Asn1Object.ControlValue = new Asn1OctetString(controlValue); + } + + /// + /// Represents a simple bind request. + /// + /// + public class LdapBindRequest : LdapMessage { + /// + /// Initializes a new instance of the class. + /// Constructs a simple bind request. + /// + /// The Ldap protocol version, use Ldap_V3. + /// Ldap_V2 is not supported. + /// If non-null and non-empty, specifies that the + /// connection and all operations through it should + /// be authenticated with dn as the distinguished + /// name. + /// If non-null and non-empty, specifies that the + /// connection and all operations through it should + /// be authenticated with dn as the distinguished + /// name and passwd as password. + public LdapBindRequest(Int32 version, String dn, SByte[] password) + : base(LdapOperation.BindRequest, new RfcBindRequest(version, dn, password)) { + } + + /// + /// Retrieves the Authentication DN for a bind request. + /// + /// + /// The authentication dn. + /// + public String AuthenticationDN => this.Asn1Object.RequestDn; + + /// + public override String ToString() => this.Asn1Object.ToString(); + } + + /// + /// Encapsulates a continuation reference from an asynchronous search operation. + /// + /// + internal class LdapSearchResultReference : LdapMessage { + /// + /// Initializes a new instance of the class. + /// Constructs an LdapSearchResultReference object. + /// + /// The LdapMessage with a search reference. + internal LdapSearchResultReference(RfcLdapMessage message) + : base(message) { + } + + /// + /// Returns any URLs in the object. + /// + /// + /// The referrals. + /// + public String[] Referrals { + get { + Asn1Object[] references = ((RfcSearchResultReference)this.Message.Response).ToArray(); + String[] srefs = new String[references.Length]; + for(Int32 i = 0; i < references.Length; i++) { + srefs[i] = ((Asn1OctetString)references[i]).StringValue(); + } + + return srefs; + } + } + } + + internal class LdapResponse : LdapMessage { + internal LdapResponse(RfcLdapMessage message) + : base(message) { + } + + public String ErrorMessage => ((IRfcResponse)this.Message.Response).GetErrorMessage().StringValue(); + + public String MatchedDN => ((IRfcResponse)this.Message.Response).GetMatchedDN().StringValue(); + + public LdapStatusCode ResultCode => this.Message.Response is RfcSearchResultEntry || + (IRfcResponse)this.Message.Response is RfcIntermediateResponse + ? LdapStatusCode.Success + : (LdapStatusCode)((IRfcResponse)this.Message.Response).GetResultCode().IntValue(); + + internal LdapException Exception { + get; set; + } + + internal void ChkResultCode() { + if(this.Exception != null) { + throw this.Exception; + } + + switch(this.ResultCode) { + case LdapStatusCode.Success: + case LdapStatusCode.CompareTrue: + case LdapStatusCode.CompareFalse: + break; + case LdapStatusCode.Referral: + throw new LdapException( + "Automatic referral following not enabled", + LdapStatusCode.Referral, + this.ErrorMessage); + default: + throw new LdapException(this.ResultCode.ToString().Humanize(), this.ResultCode, this.ErrorMessage, this.MatchedDN); + } + } + } + + /// + /// The RespControlVector class implements extends the + /// existing Vector class so that it can be used to maintain a + /// list of currently registered control responses. + /// + internal class RespControlVector : List { + private readonly Object _syncLock = new Object(); + + public RespControlVector(Int32 cap) + : base(cap) { + } + + public void RegisterResponseControl(String oid, Type controlClass) { + lock(this._syncLock) { + this.Add(new RegisteredControl(this, oid, controlClass)); + } + } + + /// + /// Inner class defined to create a temporary object to encapsulate + /// all registration information about a response control. + /// + internal class RegisteredControl { + public RegisteredControl(RespControlVector enclosingInstance, String oid, Type controlClass) { + this.EnclosingInstance = enclosingInstance; + this.MyOid = oid; + this.MyClass = controlClass; + } + + internal Type MyClass { + get; + } + + internal String MyOid { + get; + } + + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0052:Ungelesene private Member entfernen", Justification = "")] + private RespControlVector EnclosingInstance { + get; + } + } + } + + /// + /// Represents and Ldap Bind Request. + ///
    +  /// BindRequest ::= [APPLICATION 0] SEQUENCE {
    +  /// version                 INTEGER (1 .. 127),
    +  /// name                    LdapDN,
    +  /// authentication          AuthenticationChoice }
    +  /// 
    + /// + /// + internal sealed class RfcBindRequest + : Asn1Sequence, IRfcRequest { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0052:Ungelesene private Member entfernen", Justification = "")] + private readonly SByte[] _password; + private static readonly Asn1Identifier Id = new Asn1Identifier(LdapOperation.BindRequest); + + public RfcBindRequest(Int32 version, String name, SByte[] password) + : base(3) { + this._password = password; + this.Add(new Asn1Integer(version)); + this.Add(name); + this.Add(new RfcAuthenticationChoice(password)); + } + + public Asn1Integer Version { + get => (Asn1Integer)this.Get(0); + set => this.Set(0, value); + } + + public Asn1OctetString Name { + get => (Asn1OctetString)this.Get(1); + set => this.Set(1, value); + } + + public RfcAuthenticationChoice AuthenticationChoice { + get => (RfcAuthenticationChoice)this.Get(2); + set => this.Set(2, value); + } + + public override Asn1Identifier GetIdentifier() => Id; + + public String GetRequestDN() => ((Asn1OctetString)this.Get(1)).StringValue(); + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/Ldap/LdapEntry.cs b/Unosquare.Swan/Networking/Ldap/LdapEntry.cs index b1d7f34..36837c3 100644 --- a/Unosquare.Swan/Networking/Ldap/LdapEntry.cs +++ b/Unosquare.Swan/Networking/Ldap/LdapEntry.cs @@ -1,794 +1,734 @@ -namespace Unosquare.Swan.Networking.Ldap -{ - using System.Linq; - using System; - using System.Collections.Generic; - using System.Text; - +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; + +namespace Unosquare.Swan.Networking.Ldap { + /// + /// Represents a single entry in a directory, consisting of + /// a distinguished name (DN) and zero or more attributes. + /// An instance of + /// LdapEntry is created in order to add an entry to a directory, and + /// instances of LdapEntry are returned on a search by enumerating an + /// LdapSearchResults. + /// + /// + /// + public class LdapEntry { + private readonly LdapAttributeSet _attrs; + /// - /// Represents a single entry in a directory, consisting of - /// a distinguished name (DN) and zero or more attributes. - /// An instance of - /// LdapEntry is created in order to add an entry to a directory, and - /// instances of LdapEntry are returned on a search by enumerating an - /// LdapSearchResults. + /// Initializes a new instance of the class. + /// Constructs a new entry with the specified distinguished name and set + /// of attributes. /// - /// - /// - public class LdapEntry - { - private readonly LdapAttributeSet _attrs; - - /// - /// Initializes a new instance of the class. - /// Constructs a new entry with the specified distinguished name and set - /// of attributes. - /// - /// The distinguished name of the new entry. The - /// value is not validated. An invalid distinguished - /// name will cause operations using this entry to fail. - /// The initial set of attributes assigned to the - /// entry. - public LdapEntry(string dn = null, LdapAttributeSet attrs = null) - { - DN = dn ?? string.Empty; - _attrs = attrs ?? new LdapAttributeSet(); - } - - /// - /// Returns the distinguished name of the entry. - /// - /// - /// The dn. - /// - public string DN { get; } - - /// - /// Returns the attributes matching the specified attrName. - /// - /// The name of the attribute or attributes to return. - /// - /// The attribute matching the name. - /// - public LdapAttribute GetAttribute(string attrName) => _attrs[attrName]; - - /// - /// Returns the attribute set of the entry. - /// All base and subtype variants of all attributes are - /// returned. The LdapAttributeSet returned may be - /// empty if there are no attributes in the entry. - /// - /// - /// The attribute set of the entry. - /// - public LdapAttributeSet GetAttributeSet() => _attrs; - - /// - /// Returns an attribute set from the entry, consisting of only those - /// attributes matching the specified subtypes. - /// The getAttributeSet method can be used to extract only - /// a particular language variant subtype of each attribute, - /// if it exists. The "subtype" may be, for example, "lang-ja", "binary", - /// or "lang-ja;phonetic". If more than one subtype is specified, separated - /// with a semicolon, only those attributes with all of the named - /// subtypes will be returned. The LdapAttributeSet returned may be - /// empty if there are no matching attributes in the entry. - /// - /// One or more subtype specification(s), separated - /// with semicolons. The "lang-ja" and - /// "lang-en;phonetic" are valid subtype - /// specifications. - /// - /// An attribute set from the entry with the attributes that - /// match the specified subtypes or an empty set if no attributes - /// match. - /// - public LdapAttributeSet GetAttributeSet(string subtype) => _attrs.GetSubset(subtype); - } - + /// The distinguished name of the new entry. The + /// value is not validated. An invalid distinguished + /// name will cause operations using this entry to fail. + /// The initial set of attributes assigned to the + /// entry. + public LdapEntry(String dn = null, LdapAttributeSet attrs = null) { + this.DN = dn ?? String.Empty; + this._attrs = attrs ?? new LdapAttributeSet(); + } + /// - /// The name and values of one attribute of a directory entry. - /// LdapAttribute objects are used when searching for, adding, - /// modifying, and deleting attributes from the directory. - /// LdapAttributes are often used in conjunction with an - /// LdapAttributeSet when retrieving or adding multiple - /// attributes to an entry. + /// Returns the distinguished name of the entry. /// - public class LdapAttribute - { - private readonly string _baseName; // cn of cn;lang-ja;phonetic - private readonly string[] _subTypes; // lang-ja of cn;lang-ja - private object[] _values; // Array of byte[] attribute values - - /// - /// Initializes a new instance of the class. - /// Constructs an attribute with no values. - /// - /// Name of the attribute. - /// Attribute name cannot be null. - public LdapAttribute(string attrName) - { - Name = attrName ?? throw new ArgumentNullException(nameof(attrName)); - _baseName = GetBaseName(attrName); - _subTypes = GetSubtypes(attrName); - } - - /// - /// Initializes a new instance of the class. - /// Constructs an attribute with a single value. - /// - /// Name of the attribute. - /// Value of the attribute as a string. - /// Attribute value cannot be null. - public LdapAttribute(string attrName, string attrString) - : this(attrName) - { - Add(Encoding.UTF8.GetSBytes(attrString)); - } - - /// - /// Returns the values of the attribute as an array of bytes. - /// - /// - /// The byte value array. - /// - public sbyte[][] ByteValueArray - { - get - { - if (_values == null) - return new sbyte[0][]; - - var size = _values.Length; - var bva = new sbyte[size][]; - - // Deep copy so application cannot change values - for (int i = 0, u = size; i < u; i++) - { - bva[i] = new sbyte[((sbyte[])_values[i]).Length]; - Array.Copy((Array)_values[i], 0, bva[i], 0, bva[i].Length); - } - - return bva; - } - } - - /// - /// Returns the values of the attribute as an array of strings. - /// - /// - /// The string value array. - /// - public string[] StringValueArray - { - get - { - if (_values == null) - return new string[0]; - - var size = _values.Length; - var sva = new string[size]; - - for (var j = 0; j < size; j++) - { - sva[j] = Encoding.UTF8.GetString((sbyte[])_values[j]); - } - - return sva; - } - } - - /// - /// Returns the the first value of the attribute as an UTF-8 string. - /// - /// - /// The string value. - /// - public string StringValue => _values == null ? null : Encoding.UTF8.GetString((sbyte[])_values[0]); - - /// - /// Returns the first value of the attribute as a byte array or null. - /// - /// - /// The byte value. - /// - public sbyte[] ByteValue - { - get - { - if (_values == null) return null; - - // Deep copy so app can't change the value - var bva = new sbyte[((sbyte[])_values[0]).Length]; - Array.Copy((Array)_values[0], 0, bva, 0, bva.Length); - - return bva; - } - } - - /// - /// Returns the language subtype of the attribute, if any. - /// For example, if the attribute name is cn;lang-ja;phonetic, - /// this method returns the string, lang-ja. - /// - /// - /// The language subtype. - /// - public string LangSubtype => _subTypes?.FirstOrDefault(t => t.StartsWith("lang-")); - - /// - /// Returns the name of the attribute. - /// - /// - /// The name. - /// - public string Name { get; } - - internal string Value - { - set - { - _values = null; - - Add(Encoding.UTF8.GetSBytes(value)); - } - } - - /// - /// Extracts the subtypes from the specified attribute name. - /// For example, if the attribute name is cn;lang-ja;phonetic, - /// this method returns an array containing lang-ja and phonetic. - /// - /// Name of the attribute from which to extract - /// the subtypes. - /// - /// An array subtypes or null if the attribute has none. - /// - /// Attribute name cannot be null. - public static string[] GetSubtypes(string attrName) - { - if (attrName == null) - { - throw new ArgumentException("Attribute name cannot be null"); - } - - var st = new Tokenizer(attrName, ";"); - string[] subTypes = null; - var cnt = st.Count; - - if (cnt > 0) - { - st.NextToken(); // skip over basename - subTypes = new string[cnt - 1]; - var i = 0; - while (st.HasMoreTokens()) - { - subTypes[i++] = st.NextToken(); - } - } - - return subTypes; - } - - /// - /// Returns the base name of the specified attribute name. - /// For example, if the attribute name is cn;lang-ja;phonetic, - /// this method returns cn. - /// - /// Name of the attribute from which to extract the - /// base name. - /// The base name of the attribute. - /// Attribute name cannot be null. - public static string GetBaseName(string attrName) - { - if (attrName == null) - { - throw new ArgumentException("Attribute name cannot be null"); - } - - var idx = attrName.IndexOf(';'); - return idx == -1 ? attrName : attrName.Substring(0, idx - 0); - } - - /// - /// Clones this instance. - /// - /// A cloned instance. - public LdapAttribute Clone() - { - var newObj = MemberwiseClone(); - if (_values != null) - { - Array.Copy(_values, 0, ((LdapAttribute)newObj)._values, 0, _values.Length); - } - - return (LdapAttribute) newObj; - } - - /// - /// Adds a value to the attribute. - /// - /// Value of the attribute as a String. - /// Attribute value cannot be null. - public void AddValue(string attrString) - { - if (attrString == null) - { - throw new ArgumentException("Attribute value cannot be null"); - } - - Add(Encoding.UTF8.GetSBytes(attrString)); - } - - /// - /// Adds a byte-formatted value to the attribute. - /// - /// Value of the attribute as raw bytes. - /// Note: If attrBytes represents a string it should be UTF-8 encoded. - /// Attribute value cannot be null. - public void AddValue(sbyte[] attrBytes) - { - if (attrBytes == null) - { - throw new ArgumentException("Attribute value cannot be null"); - } - - Add(attrBytes); - } - - /// - /// Adds a base64 encoded value to the attribute. - /// The value will be decoded and stored as bytes. String - /// data encoded as a base64 value must be UTF-8 characters. - /// - /// The base64 value of the attribute as a String. - /// Attribute value cannot be null. - public void AddBase64Value(string attrString) - { - if (attrString == null) - { - throw new ArgumentException("Attribute value cannot be null"); - } - - Add(Convert.FromBase64String(attrString).ToSByteArray()); - } - - /// - /// Adds a base64 encoded value to the attribute. - /// The value will be decoded and stored as bytes. Character - /// data encoded as a base64 value must be UTF-8 characters. - /// - /// The base64 value of the attribute as a StringBuffer. - /// The start index of base64 encoded part, inclusive. - /// The end index of base encoded part, exclusive. - /// attrString. - public void AddBase64Value(StringBuilder attrString, int start, int end) - { - if (attrString == null) - { - throw new ArgumentNullException(nameof(attrString)); - } - - Add(Convert.FromBase64String(attrString.ToString(start, end)).ToSByteArray()); - } - - /// - /// Adds a base64 encoded value to the attribute. - /// The value will be decoded and stored as bytes. Character - /// data encoded as a base64 value must be UTF-8 characters. - /// - /// The base64 value of the attribute as an array of - /// characters. - /// attrChars. - public void AddBase64Value(char[] attrChars) - { - if (attrChars == null) - { - throw new ArgumentNullException(nameof(attrChars)); - } - - Add(Convert.FromBase64CharArray(attrChars, 0, attrChars.Length).ToSByteArray()); - } - - /// - /// Returns the base name of the attribute. - /// For example, if the attribute name is cn;lang-ja;phonetic, - /// this method returns cn. - /// - /// - /// The base name of the attribute. - /// - public string GetBaseName() => _baseName; - - /// - /// Extracts the subtypes from the attribute name. - /// For example, if the attribute name is cn;lang-ja;phonetic, - /// this method returns an array containing lang-ja and phonetic. - /// - /// - /// An array subtypes or null if the attribute has none. - /// - public string[] GetSubtypes() => _subTypes; - - /// - /// Reports if the attribute name contains the specified subtype. - /// For example, if you check for the subtype lang-en and the - /// attribute name is cn;lang-en, this method returns true. - /// - /// - /// The single subtype to check for. - /// - /// - /// True, if the attribute has the specified subtype; - /// false, if it doesn't. - /// - public bool HasSubtype(string subtype) - { - if (subtype == null) - { - throw new ArgumentNullException(nameof(subtype)); - } - - return _subTypes != null && _subTypes.Any(t => string.Equals(t, subtype, StringComparison.OrdinalIgnoreCase)); - } - - /// - /// Reports if the attribute name contains all the specified subtypes. - /// For example, if you check for the subtypes lang-en and phonetic - /// and if the attribute name is cn;lang-en;phonetic, this method - /// returns true. If the attribute name is cn;phonetic or cn;lang-en, - /// this method returns false. - /// - /// - /// An array of subtypes to check for. - /// - /// - /// True, if the attribute has all the specified subtypes; - /// false, if it doesn't have all the subtypes. - /// - public bool HasSubtypes(string[] subtypes) - { - if (subtypes == null) - { - throw new ArgumentNullException(nameof(subtypes)); - } - - for (var i = 0; i < subtypes.Length; i++) - { - foreach (var sub in _subTypes) - { - if (sub == null) - { - throw new ArgumentException($"subtype at array index {i} cannot be null"); - } - - if (string.Equals(sub, subtypes[i], StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - } - - return false; - } - - /// - /// Removes a string value from the attribute. - /// - /// Value of the attribute as a string. - /// Note: Removing a value which is not present in the attribute has - /// no effect. - /// attrString. - public void RemoveValue(string attrString) - { - if (attrString == null) - { - throw new ArgumentNullException(nameof(attrString)); - } - - RemoveValue(Encoding.UTF8.GetSBytes(attrString)); - } - - /// - /// Removes a byte-formatted value from the attribute. - /// - /// Value of the attribute as raw bytes. - /// Note: If attrBytes represents a string it should be UTF-8 encoded. - /// Note: Removing a value which is not present in the attribute has - /// no effect. - /// - /// attrBytes. - public void RemoveValue(sbyte[] attrBytes) - { - if (attrBytes == null) - { - throw new ArgumentNullException(nameof(attrBytes)); - } - - for (var i = 0; i < _values.Length; i++) - { - if (!Equals(attrBytes, (sbyte[])_values[i])) continue; - - if (i == 0 && _values.Length == 1) - { - // Optimize if first element of a single valued attr - _values = null; - return; - } - - if (_values.Length == 1) - { - _values = null; - } - else - { - var moved = _values.Length - i - 1; - var tmp = new object[_values.Length - 1]; - if (i != 0) - { - Array.Copy(_values, 0, tmp, 0, i); - } - - if (moved != 0) - { - Array.Copy(_values, i + 1, tmp, i, moved); - } - - _values = tmp; - } - - break; - } - } - - /// - /// Returns the number of values in the attribute. - /// - /// - /// The number of values in the attribute. - /// - public int Size() => _values?.Length ?? 0; - - /// - /// Compares this object with the specified object for order. - /// Ordering is determined by comparing attribute names using the method Compare() of the String class. - /// - /// The LdapAttribute to be compared to this object. - /// - /// Returns a negative integer, zero, or a positive - /// integer as this object is less than, equal to, or greater than the - /// specified object. - /// - public int CompareTo(object attribute) - => string.Compare(Name, ((LdapAttribute)attribute).Name, StringComparison.Ordinal); - - /// - /// Returns a string representation of this LdapAttribute. - /// - /// - /// a string representation of this LdapAttribute. - /// - /// NullReferenceException. - public override string ToString() - { - var result = new StringBuilder("LdapAttribute: "); - - result.Append("{type='" + Name + "'"); - - if (_values != null) - { - result - .Append(", ") - .Append(_values.Length == 1 ? "value='" : "values='"); - - for (var i = 0; i < _values.Length; i++) - { - if (i != 0) - { - result.Append("','"); - } - - if (((sbyte[])_values[i]).Length == 0) - { - continue; - } - - var sval = Encoding.UTF8.GetString((sbyte[])_values[i]); - if (sval.Length == 0) - { - // didn't decode well, must be binary - result.Append(" - /// Adds an object to this object's list of attribute values. - /// - /// Ultimately all of this attribute's values are treated - /// as binary data so we simplify the process by requiring - /// that all data added to our list is in binary form. - /// Note: If attrBytes represents a string it should be UTF-8 encoded. - private void Add(sbyte[] bytes) - { - if (_values == null) - { - _values = new object[] { bytes }; - } - else - { - // Duplicate attribute values not allowed - if (_values.Any(t => Equals(bytes, (sbyte[])t))) - { - return; // Duplicate, don't add - } - - var tmp = new object[_values.Length + 1]; - Array.Copy(_values, 0, tmp, 0, _values.Length); - tmp[_values.Length] = bytes; - _values = tmp; - } - } - - private static bool Equals(sbyte[] e1, sbyte[] e2) - { - // If same object, they compare true - if (e1 == e2) - return true; - - // If either but not both are null, they compare false - if (e1 == null || e2 == null) - return false; - - // If arrays have different length, they compare false - var length = e1.Length; - if (e2.Length != length) - return false; - - // If any of the bytes are different, they compare false - for (var i = 0; i < length; i++) - { - if (e1[i] != e2[i]) - return false; - } - - return true; - } - } - + /// + /// The dn. + /// + public String DN { + get; + } + /// - /// A set of LdapAttribute objects. - /// An LdapAttributeSet is a collection of LdapAttribute - /// classes as returned from an LdapEntry on a search or read - /// operation. LdapAttributeSet may be also used to construct an entry - /// to be added to a directory. + /// Returns the attributes matching the specified attrName. /// - /// - /// - public class LdapAttributeSet : Dictionary - { - /// - /// Initializes a new instance of the class. - /// - public LdapAttributeSet() - : base(StringComparer.OrdinalIgnoreCase) - { - // placeholder - } - - /// - /// Creates a new attribute set containing only the attributes that have - /// the specified subtypes. - /// For example, suppose an attribute set contains the following - /// attributes: - ///
    • cn
    • cn;lang-ja
    • sn;phonetic;lang-ja
    • sn;lang-us
    - /// Calling the getSubset method and passing lang-ja as the - /// argument, the method returns an attribute set containing the following - /// attributes:. - ///
    • cn;lang-ja
    • sn;phonetic;lang-ja
    - ///
    - /// Semi-colon delimited list of subtypes to include. For - /// example: - ///
    • "lang-ja" specifies only Japanese language subtypes
    • "binary" specifies only binary subtypes
    • - /// "binary;lang-ja" specifies only Japanese language subtypes - /// which also are binary - ///
    - /// Note: Novell eDirectory does not currently support language subtypes. - /// It does support the "binary" subtype. - /// - /// An attribute set containing the attributes that match the - /// specified subtype. - /// - public LdapAttributeSet GetSubset(string subtype) - { - // Create a new tempAttributeSet - var tempAttributeSet = new LdapAttributeSet(); - - foreach (var kvp in this) - { - if (kvp.Value.HasSubtype(subtype)) - tempAttributeSet.Add(kvp.Value.Clone()); - } - - return tempAttributeSet; - } - - /// - /// Returns true if this set contains an attribute of the same name - /// as the specified attribute. - /// - /// Object of type LdapAttribute. - /// - /// true if this set contains the specified attribute. - /// - public bool Contains(object attr) => ContainsKey(((LdapAttribute)attr).Name); - - /// - /// Adds the specified attribute to this set if it is not already present. - /// If an attribute with the same name already exists in the set then the - /// specified attribute will not be added. - /// - /// Object of type LdapAttribute. - /// - /// true if the attribute was added. - /// - public bool Add(LdapAttribute attr) - { - var name = attr.Name; - - if (ContainsKey(name)) - return false; - - this[name] = attr; - return true; - } - - /// - /// Removes the specified object from this set if it is present. - /// If the specified object is of type LdapAttribute, the - /// specified attribute will be removed. If the specified object is of type - /// string, the attribute with a name that matches the string will - /// be removed. - /// - /// The entry. - /// - /// true if the object was removed. - /// - public bool Remove(LdapAttribute entry) => Remove(entry.Name); - - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public override string ToString() - { - var retValue = new StringBuilder("LdapAttributeSet: "); - var first = true; - - foreach (var attr in this) - { - if (!first) - { - retValue.Append(" "); - } - - first = false; - retValue.Append(attr); - } - - return retValue.ToString(); - } - } + /// The name of the attribute or attributes to return. + /// + /// The attribute matching the name. + /// + public LdapAttribute GetAttribute(String attrName) => this._attrs[attrName]; + + /// + /// Returns the attribute set of the entry. + /// All base and subtype variants of all attributes are + /// returned. The LdapAttributeSet returned may be + /// empty if there are no attributes in the entry. + /// + /// + /// The attribute set of the entry. + /// + public LdapAttributeSet GetAttributeSet() => this._attrs; + + /// + /// Returns an attribute set from the entry, consisting of only those + /// attributes matching the specified subtypes. + /// The getAttributeSet method can be used to extract only + /// a particular language variant subtype of each attribute, + /// if it exists. The "subtype" may be, for example, "lang-ja", "binary", + /// or "lang-ja;phonetic". If more than one subtype is specified, separated + /// with a semicolon, only those attributes with all of the named + /// subtypes will be returned. The LdapAttributeSet returned may be + /// empty if there are no matching attributes in the entry. + /// + /// One or more subtype specification(s), separated + /// with semicolons. The "lang-ja" and + /// "lang-en;phonetic" are valid subtype + /// specifications. + /// + /// An attribute set from the entry with the attributes that + /// match the specified subtypes or an empty set if no attributes + /// match. + /// + public LdapAttributeSet GetAttributeSet(String subtype) => this._attrs.GetSubset(subtype); + } + + /// + /// The name and values of one attribute of a directory entry. + /// LdapAttribute objects are used when searching for, adding, + /// modifying, and deleting attributes from the directory. + /// LdapAttributes are often used in conjunction with an + /// LdapAttributeSet when retrieving or adding multiple + /// attributes to an entry. + /// + public class LdapAttribute { + private readonly String _baseName; // cn of cn;lang-ja;phonetic + private readonly String[] _subTypes; // lang-ja of cn;lang-ja + private Object[] _values; // Array of byte[] attribute values + + /// + /// Initializes a new instance of the class. + /// Constructs an attribute with no values. + /// + /// Name of the attribute. + /// Attribute name cannot be null. + public LdapAttribute(String attrName) { + this.Name = attrName ?? throw new ArgumentNullException(nameof(attrName)); + this._baseName = GetBaseName(attrName); + this._subTypes = GetSubtypes(attrName); + } + + /// + /// Initializes a new instance of the class. + /// Constructs an attribute with a single value. + /// + /// Name of the attribute. + /// Value of the attribute as a string. + /// Attribute value cannot be null. + public LdapAttribute(String attrName, String attrString) + : this(attrName) => this.Add(Encoding.UTF8.GetSBytes(attrString)); + + /// + /// Returns the values of the attribute as an array of bytes. + /// + /// + /// The byte value array. + /// + public SByte[][] ByteValueArray { + get { + if(this._values == null) { + return new SByte[0][]; + } + + Int32 size = this._values.Length; + SByte[][] bva = new SByte[size][]; + + // Deep copy so application cannot change values + for(Int32 i = 0, u = size; i < u; i++) { + bva[i] = new SByte[((SByte[])this._values[i]).Length]; + Array.Copy((Array)this._values[i], 0, bva[i], 0, bva[i].Length); + } + + return bva; + } + } + + /// + /// Returns the values of the attribute as an array of strings. + /// + /// + /// The string value array. + /// + public String[] StringValueArray { + get { + if(this._values == null) { + return new String[0]; + } + + Int32 size = this._values.Length; + String[] sva = new String[size]; + + for(Int32 j = 0; j < size; j++) { + sva[j] = Encoding.UTF8.GetString((SByte[])this._values[j]); + } + + return sva; + } + } + + /// + /// Returns the the first value of the attribute as an UTF-8 string. + /// + /// + /// The string value. + /// + public String StringValue => this._values == null ? null : Encoding.UTF8.GetString((SByte[])this._values[0]); + + /// + /// Returns the first value of the attribute as a byte array or null. + /// + /// + /// The byte value. + /// + public SByte[] ByteValue { + get { + if(this._values == null) { + return null; + } + + // Deep copy so app can't change the value + SByte[] bva = new SByte[((SByte[])this._values[0]).Length]; + Array.Copy((Array)this._values[0], 0, bva, 0, bva.Length); + + return bva; + } + } + + /// + /// Returns the language subtype of the attribute, if any. + /// For example, if the attribute name is cn;lang-ja;phonetic, + /// this method returns the string, lang-ja. + /// + /// + /// The language subtype. + /// + public String LangSubtype => this._subTypes?.FirstOrDefault(t => t.StartsWith("lang-")); + + /// + /// Returns the name of the attribute. + /// + /// + /// The name. + /// + public String Name { + get; + } + + internal String Value { + set { + this._values = null; + + this.Add(Encoding.UTF8.GetSBytes(value)); + } + } + + /// + /// Extracts the subtypes from the specified attribute name. + /// For example, if the attribute name is cn;lang-ja;phonetic, + /// this method returns an array containing lang-ja and phonetic. + /// + /// Name of the attribute from which to extract + /// the subtypes. + /// + /// An array subtypes or null if the attribute has none. + /// + /// Attribute name cannot be null. + public static String[] GetSubtypes(String attrName) { + if(attrName == null) { + throw new ArgumentException("Attribute name cannot be null"); + } + + Tokenizer st = new Tokenizer(attrName, ";"); + String[] subTypes = null; + Int32 cnt = st.Count; + + if(cnt > 0) { + _ = st.NextToken(); // skip over basename + subTypes = new String[cnt - 1]; + Int32 i = 0; + while(st.HasMoreTokens()) { + subTypes[i++] = st.NextToken(); + } + } + + return subTypes; + } + + /// + /// Returns the base name of the specified attribute name. + /// For example, if the attribute name is cn;lang-ja;phonetic, + /// this method returns cn. + /// + /// Name of the attribute from which to extract the + /// base name. + /// The base name of the attribute. + /// Attribute name cannot be null. + public static String GetBaseName(String attrName) { + if(attrName == null) { + throw new ArgumentException("Attribute name cannot be null"); + } + + Int32 idx = attrName.IndexOf(';'); + return idx == -1 ? attrName : attrName.Substring(0, idx - 0); + } + + /// + /// Clones this instance. + /// + /// A cloned instance. + public LdapAttribute Clone() { + Object newObj = this.MemberwiseClone(); + if(this._values != null) { + Array.Copy(this._values, 0, ((LdapAttribute)newObj)._values, 0, this._values.Length); + } + + return (LdapAttribute)newObj; + } + + /// + /// Adds a value to the attribute. + /// + /// Value of the attribute as a String. + /// Attribute value cannot be null. + public void AddValue(String attrString) { + if(attrString == null) { + throw new ArgumentException("Attribute value cannot be null"); + } + + this.Add(Encoding.UTF8.GetSBytes(attrString)); + } + + /// + /// Adds a byte-formatted value to the attribute. + /// + /// Value of the attribute as raw bytes. + /// Note: If attrBytes represents a string it should be UTF-8 encoded. + /// Attribute value cannot be null. + public void AddValue(SByte[] attrBytes) { + if(attrBytes == null) { + throw new ArgumentException("Attribute value cannot be null"); + } + + this.Add(attrBytes); + } + + /// + /// Adds a base64 encoded value to the attribute. + /// The value will be decoded and stored as bytes. String + /// data encoded as a base64 value must be UTF-8 characters. + /// + /// The base64 value of the attribute as a String. + /// Attribute value cannot be null. + public void AddBase64Value(String attrString) { + if(attrString == null) { + throw new ArgumentException("Attribute value cannot be null"); + } + + this.Add(Convert.FromBase64String(attrString).ToSByteArray()); + } + + /// + /// Adds a base64 encoded value to the attribute. + /// The value will be decoded and stored as bytes. Character + /// data encoded as a base64 value must be UTF-8 characters. + /// + /// The base64 value of the attribute as a StringBuffer. + /// The start index of base64 encoded part, inclusive. + /// The end index of base encoded part, exclusive. + /// attrString. + public void AddBase64Value(StringBuilder attrString, Int32 start, Int32 end) { + if(attrString == null) { + throw new ArgumentNullException(nameof(attrString)); + } + + this.Add(Convert.FromBase64String(attrString.ToString(start, end)).ToSByteArray()); + } + + /// + /// Adds a base64 encoded value to the attribute. + /// The value will be decoded and stored as bytes. Character + /// data encoded as a base64 value must be UTF-8 characters. + /// + /// The base64 value of the attribute as an array of + /// characters. + /// attrChars. + public void AddBase64Value(Char[] attrChars) { + if(attrChars == null) { + throw new ArgumentNullException(nameof(attrChars)); + } + + this.Add(Convert.FromBase64CharArray(attrChars, 0, attrChars.Length).ToSByteArray()); + } + + /// + /// Returns the base name of the attribute. + /// For example, if the attribute name is cn;lang-ja;phonetic, + /// this method returns cn. + /// + /// + /// The base name of the attribute. + /// + public String GetBaseName() => this._baseName; + + /// + /// Extracts the subtypes from the attribute name. + /// For example, if the attribute name is cn;lang-ja;phonetic, + /// this method returns an array containing lang-ja and phonetic. + /// + /// + /// An array subtypes or null if the attribute has none. + /// + public String[] GetSubtypes() => this._subTypes; + + /// + /// Reports if the attribute name contains the specified subtype. + /// For example, if you check for the subtype lang-en and the + /// attribute name is cn;lang-en, this method returns true. + /// + /// + /// The single subtype to check for. + /// + /// + /// True, if the attribute has the specified subtype; + /// false, if it doesn't. + /// + public Boolean HasSubtype(String subtype) { + if(subtype == null) { + throw new ArgumentNullException(nameof(subtype)); + } + + return this._subTypes != null && this._subTypes.Any(t => String.Equals(t, subtype, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Reports if the attribute name contains all the specified subtypes. + /// For example, if you check for the subtypes lang-en and phonetic + /// and if the attribute name is cn;lang-en;phonetic, this method + /// returns true. If the attribute name is cn;phonetic or cn;lang-en, + /// this method returns false. + /// + /// + /// An array of subtypes to check for. + /// + /// + /// True, if the attribute has all the specified subtypes; + /// false, if it doesn't have all the subtypes. + /// + public Boolean HasSubtypes(String[] subtypes) { + if(subtypes == null) { + throw new ArgumentNullException(nameof(subtypes)); + } + + for(Int32 i = 0; i < subtypes.Length; i++) { + foreach(String sub in this._subTypes) { + if(sub == null) { + throw new ArgumentException($"subtype at array index {i} cannot be null"); + } + + if(String.Equals(sub, subtypes[i], StringComparison.OrdinalIgnoreCase)) { + return true; + } + } + } + + return false; + } + + /// + /// Removes a string value from the attribute. + /// + /// Value of the attribute as a string. + /// Note: Removing a value which is not present in the attribute has + /// no effect. + /// attrString. + public void RemoveValue(String attrString) { + if(attrString == null) { + throw new ArgumentNullException(nameof(attrString)); + } + + this.RemoveValue(Encoding.UTF8.GetSBytes(attrString)); + } + + /// + /// Removes a byte-formatted value from the attribute. + /// + /// Value of the attribute as raw bytes. + /// Note: If attrBytes represents a string it should be UTF-8 encoded. + /// Note: Removing a value which is not present in the attribute has + /// no effect. + /// + /// attrBytes. + public void RemoveValue(SByte[] attrBytes) { + if(attrBytes == null) { + throw new ArgumentNullException(nameof(attrBytes)); + } + + for(Int32 i = 0; i < this._values.Length; i++) { + if(!Equals(attrBytes, (SByte[])this._values[i])) { + continue; + } + + if(i == 0 && this._values.Length == 1) { + // Optimize if first element of a single valued attr + this._values = null; + return; + } + + if(this._values.Length == 1) { + this._values = null; + } else { + Int32 moved = this._values.Length - i - 1; + Object[] tmp = new Object[this._values.Length - 1]; + if(i != 0) { + Array.Copy(this._values, 0, tmp, 0, i); + } + + if(moved != 0) { + Array.Copy(this._values, i + 1, tmp, i, moved); + } + + this._values = tmp; + } + + break; + } + } + + /// + /// Returns the number of values in the attribute. + /// + /// + /// The number of values in the attribute. + /// + public Int32 Size() => this._values?.Length ?? 0; + + /// + /// Compares this object with the specified object for order. + /// Ordering is determined by comparing attribute names using the method Compare() of the String class. + /// + /// The LdapAttribute to be compared to this object. + /// + /// Returns a negative integer, zero, or a positive + /// integer as this object is less than, equal to, or greater than the + /// specified object. + /// + public Int32 CompareTo(Object attribute) + => String.Compare(this.Name, ((LdapAttribute)attribute).Name, StringComparison.Ordinal); + + /// + /// Returns a string representation of this LdapAttribute. + /// + /// + /// a string representation of this LdapAttribute. + /// + /// NullReferenceException. + public override String ToString() { + StringBuilder result = new StringBuilder("LdapAttribute: "); + + _ = result.Append("{type='" + this.Name + "'"); + + if(this._values != null) { + _ = result + .Append(", ") + .Append(this._values.Length == 1 ? "value='" : "values='"); + + for(Int32 i = 0; i < this._values.Length; i++) { + if(i != 0) { + _ = result.Append("','"); + } + + if(((SByte[])this._values[i]).Length == 0) { + continue; + } + + String sval = Encoding.UTF8.GetString((SByte[])this._values[i]); + if(sval.Length == 0) { + // didn't decode well, must be binary + _ = result.Append(" + /// Adds an object to this object's list of attribute values. + /// + /// Ultimately all of this attribute's values are treated + /// as binary data so we simplify the process by requiring + /// that all data added to our list is in binary form. + /// Note: If attrBytes represents a string it should be UTF-8 encoded. + private void Add(SByte[] bytes) { + if(this._values == null) { + this._values = new Object[] { bytes }; + } else { + // Duplicate attribute values not allowed + if(this._values.Any(t => Equals(bytes, (SByte[])t))) { + return; // Duplicate, don't add + } + + Object[] tmp = new Object[this._values.Length + 1]; + Array.Copy(this._values, 0, tmp, 0, this._values.Length); + tmp[this._values.Length] = bytes; + this._values = tmp; + } + } + + private static Boolean Equals(SByte[] e1, SByte[] e2) { + // If same object, they compare true + if(e1 == e2) { + return true; + } + + // If either but not both are null, they compare false + if(e1 == null || e2 == null) { + return false; + } + + // If arrays have different length, they compare false + Int32 length = e1.Length; + if(e2.Length != length) { + return false; + } + + // If any of the bytes are different, they compare false + for(Int32 i = 0; i < length; i++) { + if(e1[i] != e2[i]) { + return false; + } + } + + return true; + } + } + + /// + /// A set of LdapAttribute objects. + /// An LdapAttributeSet is a collection of LdapAttribute + /// classes as returned from an LdapEntry on a search or read + /// operation. LdapAttributeSet may be also used to construct an entry + /// to be added to a directory. + /// + /// + /// + public class LdapAttributeSet : Dictionary { + /// + /// Initializes a new instance of the class. + /// + public LdapAttributeSet() + : base(StringComparer.OrdinalIgnoreCase) { + // placeholder + } + + /// + /// Creates a new attribute set containing only the attributes that have + /// the specified subtypes. + /// For example, suppose an attribute set contains the following + /// attributes: + ///
    • cn
    • cn;lang-ja
    • sn;phonetic;lang-ja
    • sn;lang-us
    + /// Calling the getSubset method and passing lang-ja as the + /// argument, the method returns an attribute set containing the following + /// attributes:. + ///
    • cn;lang-ja
    • sn;phonetic;lang-ja
    + ///
    + /// Semi-colon delimited list of subtypes to include. For + /// example: + ///
    • "lang-ja" specifies only Japanese language subtypes
    • "binary" specifies only binary subtypes
    • + /// "binary;lang-ja" specifies only Japanese language subtypes + /// which also are binary + ///
    + /// Note: Novell eDirectory does not currently support language subtypes. + /// It does support the "binary" subtype. + /// + /// An attribute set containing the attributes that match the + /// specified subtype. + /// + public LdapAttributeSet GetSubset(String subtype) { + // Create a new tempAttributeSet + LdapAttributeSet tempAttributeSet = new LdapAttributeSet(); + + foreach(KeyValuePair kvp in this) { + if(kvp.Value.HasSubtype(subtype)) { + _ = tempAttributeSet.Add(kvp.Value.Clone()); + } + } + + return tempAttributeSet; + } + + /// + /// Returns true if this set contains an attribute of the same name + /// as the specified attribute. + /// + /// Object of type LdapAttribute. + /// + /// true if this set contains the specified attribute. + /// + public Boolean Contains(Object attr) => this.ContainsKey(((LdapAttribute)attr).Name); + + /// + /// Adds the specified attribute to this set if it is not already present. + /// If an attribute with the same name already exists in the set then the + /// specified attribute will not be added. + /// + /// Object of type LdapAttribute. + /// + /// true if the attribute was added. + /// + public Boolean Add(LdapAttribute attr) { + String name = attr.Name; + + if(this.ContainsKey(name)) { + return false; + } + + this[name] = attr; + return true; + } + + /// + /// Removes the specified object from this set if it is present. + /// If the specified object is of type LdapAttribute, the + /// specified attribute will be removed. If the specified object is of type + /// string, the attribute with a name that matches the string will + /// be removed. + /// + /// The entry. + /// + /// true if the object was removed. + /// + public Boolean Remove(LdapAttribute entry) => this.Remove(entry.Name); + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override String ToString() { + StringBuilder retValue = new StringBuilder("LdapAttributeSet: "); + Boolean first = true; + + foreach(KeyValuePair attr in this) { + if(!first) { + _ = retValue.Append(" "); + } + + first = false; + _ = retValue.Append(attr); + } + + return retValue.ToString(); + } + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/Ldap/LdapEnums.cs b/Unosquare.Swan/Networking/Ldap/LdapEnums.cs index ee861a6..7f82fc9 100644 --- a/Unosquare.Swan/Networking/Ldap/LdapEnums.cs +++ b/Unosquare.Swan/Networking/Ldap/LdapEnums.cs @@ -1,135 +1,130 @@ -namespace Unosquare.Swan.Networking.Ldap -{ +namespace Unosquare.Swan.Networking.Ldap { + /// + /// Ldap Modification Operators. + /// + public enum LdapModificationOp { /// - /// Ldap Modification Operators. + /// Adds the listed values to the given attribute, creating + /// the attribute if it does not already exist. /// - public enum LdapModificationOp - { - /// - /// Adds the listed values to the given attribute, creating - /// the attribute if it does not already exist. - /// - Add = 0, - - /// - /// Deletes the listed values from the given attribute, - /// removing the entire attribute (1) if no values are listed or - /// (2) if all current values of the attribute are listed for - /// deletion. - /// - Delete = 1, - - /// - /// Replaces all existing values of the given attribute - /// with the new values listed, creating the attribute if it - /// does not already exist. - /// A replace with no value deletes the entire attribute if it - /// exists, and is ignored if the attribute does not exist. - /// - Replace = 2, - } - + Add = 0, + /// - /// LDAP valid scopes. + /// Deletes the listed values from the given attribute, + /// removing the entire attribute (1) if no values are listed or + /// (2) if all current values of the attribute are listed for + /// deletion. /// - public enum LdapScope - { - /// - /// Used with search to specify that the scope of entrys to search is to - /// search only the base object. - /// - ScopeBase = 0, - - /// - /// Used with search to specify that the scope of entrys to search is to - /// search only the immediate subordinates of the base object. - /// - ScopeOne = 1, - - /// - /// Used with search to specify that the scope of entrys to search is to - /// search the base object and all entries within its subtree. - /// - ScopeSub = 2, - } - + Delete = 1, + /// - /// Substring Operators. + /// Replaces all existing values of the given attribute + /// with the new values listed, creating the attribute if it + /// does not already exist. + /// A replace with no value deletes the entire attribute if it + /// exists, and is ignored if the attribute does not exist. /// - internal enum SubstringOp - { - /// - /// Search Filter Identifier for an INITIAL component of a SUBSTRING. - /// Note: An initial SUBSTRING is represented as "value*". - /// - Initial = 0, - - /// - /// Search Filter Identifier for an ANY component of a SUBSTRING. - /// Note: An ANY SUBSTRING is represented as "*value*". - /// - Any = 1, - - /// - /// Search Filter Identifier for a FINAL component of a SUBSTRING. - /// Note: A FINAL SUBSTRING is represented as "*value". - /// - Final = 2, - } - + Replace = 2, + } + + /// + /// LDAP valid scopes. + /// + public enum LdapScope { /// - /// Filtering Operators. + /// Used with search to specify that the scope of entrys to search is to + /// search only the base object. /// - internal enum FilterOp - { - /// - /// Identifier for AND component. - /// - And = 0, - - /// - /// Identifier for OR component. - /// - Or = 1, - - /// - /// Identifier for NOT component. - /// - Not = 2, - - /// - /// Identifier for EQUALITY_MATCH component. - /// - EqualityMatch = 3, - - /// - /// Identifier for SUBSTRINGS component. - /// - Substrings = 4, - - /// - /// Identifier for GREATER_OR_EQUAL component. - /// - GreaterOrEqual = 5, - - /// - /// Identifier for LESS_OR_EQUAL component. - /// - LessOrEqual = 6, - - /// - /// Identifier for PRESENT component. - /// - Present = 7, - - /// - /// Identifier for APPROX_MATCH component. - /// - ApproxMatch = 8, - - /// - /// Identifier for EXTENSIBLE_MATCH component. - /// - ExtensibleMatch = 9, - } + ScopeBase = 0, + + /// + /// Used with search to specify that the scope of entrys to search is to + /// search only the immediate subordinates of the base object. + /// + ScopeOne = 1, + + /// + /// Used with search to specify that the scope of entrys to search is to + /// search the base object and all entries within its subtree. + /// + ScopeSub = 2, + } + + /// + /// Substring Operators. + /// + internal enum SubstringOp { + /// + /// Search Filter Identifier for an INITIAL component of a SUBSTRING. + /// Note: An initial SUBSTRING is represented as "value*". + /// + Initial = 0, + + /// + /// Search Filter Identifier for an ANY component of a SUBSTRING. + /// Note: An ANY SUBSTRING is represented as "*value*". + /// + Any = 1, + + /// + /// Search Filter Identifier for a FINAL component of a SUBSTRING. + /// Note: A FINAL SUBSTRING is represented as "*value". + /// + Final = 2, + } + + /// + /// Filtering Operators. + /// + internal enum FilterOp { + /// + /// Identifier for AND component. + /// + And = 0, + + /// + /// Identifier for OR component. + /// + Or = 1, + + /// + /// Identifier for NOT component. + /// + Not = 2, + + /// + /// Identifier for EQUALITY_MATCH component. + /// + EqualityMatch = 3, + + /// + /// Identifier for SUBSTRINGS component. + /// + Substrings = 4, + + /// + /// Identifier for GREATER_OR_EQUAL component. + /// + GreaterOrEqual = 5, + + /// + /// Identifier for LESS_OR_EQUAL component. + /// + LessOrEqual = 6, + + /// + /// Identifier for PRESENT component. + /// + Present = 7, + + /// + /// Identifier for APPROX_MATCH component. + /// + ApproxMatch = 8, + + /// + /// Identifier for EXTENSIBLE_MATCH component. + /// + ExtensibleMatch = 9, + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/Ldap/LdapMessage.cs b/Unosquare.Swan/Networking/Ldap/LdapMessage.cs index bd3449b..2a01852 100644 --- a/Unosquare.Swan/Networking/Ldap/LdapMessage.cs +++ b/Unosquare.Swan/Networking/Ldap/LdapMessage.cs @@ -1,140 +1,120 @@ -namespace Unosquare.Swan.Networking.Ldap -{ +using System; +namespace Unosquare.Swan.Networking.Ldap { + /// + /// The base class for Ldap request and response messages. + /// Subclassed by response messages used in asynchronous operations. + /// + public class LdapMessage { + internal RfcLdapMessage Message; + + private Int32 _imsgNum = -1; // This instance LdapMessage number + + private LdapOperation _messageType = LdapOperation.Unknown; + + private String _stringTag; + + internal LdapMessage() { + } + /// - /// The base class for Ldap request and response messages. - /// Subclassed by response messages used in asynchronous operations. + /// Initializes a new instance of the class. + /// Creates an LdapMessage when sending a protocol operation and sends + /// some optional controls with the message. /// - public class LdapMessage - { - internal RfcLdapMessage Message; - - private int _imsgNum = -1; // This instance LdapMessage number - - private LdapOperation _messageType = LdapOperation.Unknown; - - private string _stringTag; - - internal LdapMessage() - { - } - - /// - /// Initializes a new instance of the class. - /// Creates an LdapMessage when sending a protocol operation and sends - /// some optional controls with the message. - /// - /// The type. - /// The operation type of message. - /// The controls to use with the operation. - /// - internal LdapMessage(LdapOperation type, IRfcRequest op, LdapControl[] controls = null) - { - // Get a unique number for this request message - _messageType = type; - RfcControls asn1Ctrls = null; - - if (controls != null) - { - // Move LdapControls into an RFC 2251 Controls object. - asn1Ctrls = new RfcControls(); - - foreach (var t in controls) - { - asn1Ctrls.Add(t.Asn1Object); - } - } - - // create RFC 2251 LdapMessage - Message = new RfcLdapMessage(op, asn1Ctrls); - } - - /// - /// Initializes a new instance of the class. - /// Creates an Rfc 2251 LdapMessage when the libraries receive a response - /// from a command. - /// - /// A response message. - internal LdapMessage(RfcLdapMessage message) => Message = message; - - /// - /// Returns the message ID. The message ID is an integer value - /// identifying the Ldap request and its response. - /// - /// - /// The message identifier. - /// - public virtual int MessageId - { - get - { - if (_imsgNum == -1) - { - _imsgNum = Message.MessageId; - } - - return _imsgNum; - } - } - - /// - /// Indicates whether the message is a request or a response. - /// - /// - /// true if request; otherwise, false. - /// - public virtual bool Request => Message.IsRequest(); - - internal LdapOperation Type - { - get - { - if (_messageType == LdapOperation.Unknown) - { - _messageType = Message.Type; - } - - return _messageType; - } - } - - internal virtual RfcLdapMessage Asn1Object => Message; - - internal virtual LdapMessage RequestingMessage => Message.RequestingMessage; - - /// - /// Retrieves the identifier tag for this message. - /// An identifier can be associated with a message with the - /// setTag method. - /// Tags are set by the application and not by the API or the server. - /// If a server response isRequest() == false has no tag, - /// the tag associated with the corresponding server request is used. - /// - /// - /// The tag. - /// - public virtual string Tag - { - get - { - if (_stringTag != null) - { - return _stringTag; - } - - return Request ? null : RequestingMessage?._stringTag; - } - - set => _stringTag = value; - } - - private string Name => Type.ToString(); - - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public override string ToString() => $"{Name}({MessageId}): {Message}"; - } + /// The type. + /// The operation type of message. + /// The controls to use with the operation. + /// + internal LdapMessage(LdapOperation type, IRfcRequest op, LdapControl[] controls = null) { + // Get a unique number for this request message + this._messageType = type; + RfcControls asn1Ctrls = null; + + if(controls != null) { + // Move LdapControls into an RFC 2251 Controls object. + asn1Ctrls = new RfcControls(); + + foreach(LdapControl t in controls) { + asn1Ctrls.Add(t.Asn1Object); + } + } + + // create RFC 2251 LdapMessage + this.Message = new RfcLdapMessage(op, asn1Ctrls); + } + + /// + /// Initializes a new instance of the class. + /// Creates an Rfc 2251 LdapMessage when the libraries receive a response + /// from a command. + /// + /// A response message. + internal LdapMessage(RfcLdapMessage message) => this.Message = message; + + /// + /// Returns the message ID. The message ID is an integer value + /// identifying the Ldap request and its response. + /// + /// + /// The message identifier. + /// + public virtual Int32 MessageId { + get { + if(this._imsgNum == -1) { + this._imsgNum = this.Message.MessageId; + } + + return this._imsgNum; + } + } + + /// + /// Indicates whether the message is a request or a response. + /// + /// + /// true if request; otherwise, false. + /// + public virtual Boolean Request => this.Message.IsRequest(); + + internal LdapOperation Type { + get { + if(this._messageType == LdapOperation.Unknown) { + this._messageType = this.Message.Type; + } + + return this._messageType; + } + } + + internal virtual RfcLdapMessage Asn1Object => this.Message; + + internal virtual LdapMessage RequestingMessage => this.Message.RequestingMessage; + + /// + /// Retrieves the identifier tag for this message. + /// An identifier can be associated with a message with the + /// setTag method. + /// Tags are set by the application and not by the API or the server. + /// If a server response isRequest() == false has no tag, + /// the tag associated with the corresponding server request is used. + /// + /// + /// The tag. + /// + public virtual String Tag { + get => this._stringTag ?? (this.Request ? null : this.RequestingMessage?._stringTag); + + set => this._stringTag = value; + } + + private String Name => this.Type.ToString(); + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override String ToString() => $"{this.Name}({this.MessageId}): {this.Message}"; + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/Ldap/LdapModification.cs b/Unosquare.Swan/Networking/Ldap/LdapModification.cs index 56c6c26..492b6a6 100644 --- a/Unosquare.Swan/Networking/Ldap/LdapModification.cs +++ b/Unosquare.Swan/Networking/Ldap/LdapModification.cs @@ -1,78 +1,79 @@ -namespace Unosquare.Swan.Networking.Ldap -{ +using System; +namespace Unosquare.Swan.Networking.Ldap { + /// + /// A single add, delete, or replace operation to an LdapAttribute. + /// An LdapModification contains information on the type of modification + /// being performed, the name of the attribute to be replaced, and the new + /// value. Multiple modifications are expressed as an array of modifications, + /// i.e., LdapModification[]. + /// An LdapModification or an LdapModification array enable you to modify + /// an attribute of an Ldap entry. The entire array of modifications must + /// be performed by the server as a single atomic operation in the order they + /// are listed. No changes are made to the directory unless all the operations + /// succeed. If all succeed, a success result is returned to the application. + /// It should be noted that if the connection fails during a modification, + /// it is indeterminate whether the modification occurred or not. + /// There are three types of modification operations: Add, Delete, + /// and Replace. + /// Add: Creates the attribute if it doesn't exist, and adds + /// the specified values. This operation must contain at least one value, and + /// all values of the attribute must be unique. + /// Delete: Deletes specified values from the attribute. If no + /// values are specified, or if all existing values of the attribute are + /// specified, the attribute is removed. Mandatory attributes cannot be + /// removed. + /// Replace: Creates the attribute if necessary, and replaces + /// all existing values of the attribute with the specified values. + /// If you wish to keep any existing values of a multi-valued attribute, + /// you must include these values in the replace operation. + /// A replace operation with no value will remove the entire attribute if it + /// exists, and is ignored if the attribute does not exist. + /// Additional information on Ldap modifications is available in section 4.6 + /// of. rfc2251.txt + /// + /// + /// + public sealed class LdapModification : LdapMessage { /// - /// A single add, delete, or replace operation to an LdapAttribute. - /// An LdapModification contains information on the type of modification - /// being performed, the name of the attribute to be replaced, and the new - /// value. Multiple modifications are expressed as an array of modifications, - /// i.e., LdapModification[]. - /// An LdapModification or an LdapModification array enable you to modify - /// an attribute of an Ldap entry. The entire array of modifications must - /// be performed by the server as a single atomic operation in the order they - /// are listed. No changes are made to the directory unless all the operations - /// succeed. If all succeed, a success result is returned to the application. - /// It should be noted that if the connection fails during a modification, - /// it is indeterminate whether the modification occurred or not. - /// There are three types of modification operations: Add, Delete, - /// and Replace. - /// Add: Creates the attribute if it doesn't exist, and adds - /// the specified values. This operation must contain at least one value, and - /// all values of the attribute must be unique. - /// Delete: Deletes specified values from the attribute. If no - /// values are specified, or if all existing values of the attribute are - /// specified, the attribute is removed. Mandatory attributes cannot be - /// removed. - /// Replace: Creates the attribute if necessary, and replaces - /// all existing values of the attribute with the specified values. - /// If you wish to keep any existing values of a multi-valued attribute, - /// you must include these values in the replace operation. - /// A replace operation with no value will remove the entire attribute if it - /// exists, and is ignored if the attribute does not exist. - /// Additional information on Ldap modifications is available in section 4.6 - /// of. rfc2251.txt + /// Initializes a new instance of the class. + /// Specifies a modification to be made to an attribute. /// - /// - /// - public sealed class LdapModification : LdapMessage - { - /// - /// Initializes a new instance of the class. - /// Specifies a modification to be made to an attribute. - /// - /// The op. - /// The attribute to modify. - public LdapModification(LdapModificationOp op, LdapAttribute attr) - { - Op = op; - Attribute = attr; - } - - /// - /// Initializes a new instance of the class. - /// - /// The op. - /// Name of the attribute. - /// The attribute value. - public LdapModification(LdapModificationOp op, string attrName, string attrValue) - : this(op, new LdapAttribute(attrName, attrValue)) - { - // placeholder - } - - /// - /// Returns the attribute to modify, with any existing values. - /// - /// - /// The attribute. - /// - public LdapAttribute Attribute { get; } - - /// - /// Returns the type of modification specified by this object. - /// - /// - /// The op. - /// - public LdapModificationOp Op { get; } - } + /// The op. + /// The attribute to modify. + public LdapModification(LdapModificationOp op, LdapAttribute attr) { + this.Op = op; + this.Attribute = attr; + } + + /// + /// Initializes a new instance of the class. + /// + /// The op. + /// Name of the attribute. + /// The attribute value. + public LdapModification(LdapModificationOp op, String attrName, String attrValue) + : this(op, new LdapAttribute(attrName, attrValue)) { + // placeholder + } + + /// + /// Returns the attribute to modify, with any existing values. + /// + /// + /// The attribute. + /// + public LdapAttribute Attribute { + get; + } + + /// + /// Returns the type of modification specified by this object. + /// + /// + /// The op. + /// + public LdapModificationOp Op { + get; + } + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/Ldap/LdapModifyRequest.cs b/Unosquare.Swan/Networking/Ldap/LdapModifyRequest.cs index f8a8434..749da07 100644 --- a/Unosquare.Swan/Networking/Ldap/LdapModifyRequest.cs +++ b/Unosquare.Swan/Networking/Ldap/LdapModifyRequest.cs @@ -1,68 +1,60 @@ -namespace Unosquare.Swan.Networking.Ldap -{ +using System; +namespace Unosquare.Swan.Networking.Ldap { + /// + /// Represents a LDAP Modification Request Message. + /// + /// + public sealed class LdapModifyRequest : LdapMessage { /// - /// Represents a LDAP Modification Request Message. + /// Initializes a new instance of the class. /// - /// - public sealed class LdapModifyRequest : LdapMessage - { - /// - /// Initializes a new instance of the class. - /// - /// The dn. - /// The modifications. - /// The control. - public LdapModifyRequest(string dn, LdapModification[] modifications, LdapControl[] control) - : base(LdapOperation.ModifyRequest, new RfcModifyRequest(dn, EncodeModifications(modifications)), control) - { - } - - /// - /// Gets the dn. - /// - /// - /// The dn. - /// - public string DN => Asn1Object.RequestDn; - - /// - public override string ToString() => Asn1Object.ToString(); - - private static Asn1SequenceOf EncodeModifications(LdapModification[] mods) - { - var rfcMods = new Asn1SequenceOf(mods.Length); - - foreach (var t in mods) - { - var attr = t.Attribute; - - var vals = new Asn1SetOf(attr.Size()); - if (attr.Size() > 0) - { - foreach (var val in attr.ByteValueArray) - { - vals.Add(new Asn1OctetString(val)); - } - } - - var rfcMod = new Asn1Sequence(2); - rfcMod.Add(new Asn1Enumerated((int) t.Op)); - rfcMod.Add(new RfcAttributeTypeAndValues(attr.Name, vals)); - - rfcMods.Add(rfcMod); - } - - return rfcMods; - } - - internal class RfcAttributeTypeAndValues : Asn1Sequence - { - public RfcAttributeTypeAndValues(string type, Asn1Object vals) - : base(2) - { - Add(type); - Add(vals); - } - } - } + /// The dn. + /// The modifications. + /// The control. + public LdapModifyRequest(String dn, LdapModification[] modifications, LdapControl[] control) + : base(LdapOperation.ModifyRequest, new RfcModifyRequest(dn, EncodeModifications(modifications)), control) { + } + + /// + /// Gets the dn. + /// + /// + /// The dn. + /// + public String DN => this.Asn1Object.RequestDn; + + /// + public override String ToString() => this.Asn1Object.ToString(); + + private static Asn1SequenceOf EncodeModifications(LdapModification[] mods) { + Asn1SequenceOf rfcMods = new Asn1SequenceOf(mods.Length); + + foreach(LdapModification t in mods) { + LdapAttribute attr = t.Attribute; + + Asn1SetOf vals = new Asn1SetOf(attr.Size()); + if(attr.Size() > 0) { + foreach(SByte[] val in attr.ByteValueArray) { + vals.Add(new Asn1OctetString(val)); + } + } + + Asn1Sequence rfcMod = new Asn1Sequence(2); + rfcMod.Add(new Asn1Enumerated((Int32)t.Op)); + rfcMod.Add(new RfcAttributeTypeAndValues(attr.Name, vals)); + + rfcMods.Add(rfcMod); + } + + return rfcMods; + } + + internal class RfcAttributeTypeAndValues : Asn1Sequence { + public RfcAttributeTypeAndValues(String type, Asn1Object vals) + : base(2) { + this.Add(type); + this.Add(vals); + } + } + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/Ldap/LdapOperation.cs b/Unosquare.Swan/Networking/Ldap/LdapOperation.cs index 994be58..b0081e8 100644 --- a/Unosquare.Swan/Networking/Ldap/LdapOperation.cs +++ b/Unosquare.Swan/Networking/Ldap/LdapOperation.cs @@ -1,117 +1,114 @@ -namespace Unosquare.Swan.Networking.Ldap -{ +namespace Unosquare.Swan.Networking.Ldap { + /// + /// LDAP Operation. + /// + internal enum LdapOperation { /// - /// LDAP Operation. + /// The unknown /// - internal enum LdapOperation - { - /// - /// The unknown - /// - Unknown = -1, - - /// - /// A bind request operation. - /// BIND_REQUEST = 0 - /// - BindRequest = 0, - - /// - /// A bind response operation. - /// BIND_RESPONSE = 1 - /// - BindResponse = 1, - - /// - /// An unbind request operation. - /// UNBIND_REQUEST = 2 - /// - UnbindRequest = 2, - - /// - /// A search request operation. - /// SEARCH_REQUEST = 3 - /// - SearchRequest = 3, - - /// - /// A search response containing data. - /// SEARCH_RESPONSE = 4 - /// - SearchResponse = 4, - - /// - /// A search result message - contains search status. - /// SEARCH_RESULT = 5 - /// - SearchResult = 5, - - /// - /// A modify request operation. - /// MODIFY_REQUEST = 6 - /// - ModifyRequest = 6, - - /// - /// A modify response operation. - /// MODIFY_RESPONSE = 7 - /// - ModifyResponse = 7, - - /// - /// An abandon request operation. - /// ABANDON_REQUEST = 16 - /// - AbandonRequest = 16, - - /// - /// A search result reference operation. - /// SEARCH_RESULT_REFERENCE = 19 - /// - SearchResultReference = 19, - - /// - /// An extended request operation. - /// EXTENDED_REQUEST = 23 - /// - ExtendedRequest = 23, - - /// - /// An extended response operation. - /// EXTENDED_RESPONSE = 24 - /// - ExtendedResponse = 24, - - /// - /// An intermediate response operation. - /// INTERMEDIATE_RESPONSE = 25 - /// - IntermediateResponse = 25, - } - + Unknown = -1, + /// - /// ASN1 tags. + /// A bind request operation. + /// BIND_REQUEST = 0 /// - internal enum Asn1IdentifierTag - { - /// - /// Universal tag class. - /// - Universal = 0, - - /// - /// Application-wide tag class. - /// - Application = 1, - - /// - /// Context-specific tag class. - /// - Context = 2, - - /// - /// Private-use tag class. - /// - Private = 3, - } + BindRequest = 0, + + /// + /// A bind response operation. + /// BIND_RESPONSE = 1 + /// + BindResponse = 1, + + /// + /// An unbind request operation. + /// UNBIND_REQUEST = 2 + /// + UnbindRequest = 2, + + /// + /// A search request operation. + /// SEARCH_REQUEST = 3 + /// + SearchRequest = 3, + + /// + /// A search response containing data. + /// SEARCH_RESPONSE = 4 + /// + SearchResponse = 4, + + /// + /// A search result message - contains search status. + /// SEARCH_RESULT = 5 + /// + SearchResult = 5, + + /// + /// A modify request operation. + /// MODIFY_REQUEST = 6 + /// + ModifyRequest = 6, + + /// + /// A modify response operation. + /// MODIFY_RESPONSE = 7 + /// + ModifyResponse = 7, + + /// + /// An abandon request operation. + /// ABANDON_REQUEST = 16 + /// + AbandonRequest = 16, + + /// + /// A search result reference operation. + /// SEARCH_RESULT_REFERENCE = 19 + /// + SearchResultReference = 19, + + /// + /// An extended request operation. + /// EXTENDED_REQUEST = 23 + /// + ExtendedRequest = 23, + + /// + /// An extended response operation. + /// EXTENDED_RESPONSE = 24 + /// + ExtendedResponse = 24, + + /// + /// An intermediate response operation. + /// INTERMEDIATE_RESPONSE = 25 + /// + IntermediateResponse = 25, + } + + /// + /// ASN1 tags. + /// + internal enum Asn1IdentifierTag { + /// + /// Universal tag class. + /// + Universal = 0, + + /// + /// Application-wide tag class. + /// + Application = 1, + + /// + /// Context-specific tag class. + /// + Context = 2, + + /// + /// Private-use tag class. + /// + Private = 3, + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/Ldap/LdapSearchRequest.cs b/Unosquare.Swan/Networking/Ldap/LdapSearchRequest.cs index cb00800..95e7adc 100644 --- a/Unosquare.Swan/Networking/Ldap/LdapSearchRequest.cs +++ b/Unosquare.Swan/Networking/Ldap/LdapSearchRequest.cs @@ -1,185 +1,180 @@ -namespace Unosquare.Swan.Networking.Ldap -{ - using System.Collections; - +using System; +using System.Collections; + +namespace Unosquare.Swan.Networking.Ldap { + /// + /// Represents an Ldap Search request. + /// + /// + internal sealed class LdapSearchRequest : LdapMessage { /// - /// Represents an Ldap Search request. + /// Initializes a new instance of the class. /// - /// - internal sealed class LdapSearchRequest : LdapMessage - { - /// - /// Initializes a new instance of the class. - /// - /// The base distinguished name to search from. - /// The scope of the entries to search. The following - /// are the valid options:. - ///
    • SCOPE_BASE - searches only the base DN
    • SCOPE_ONE - searches only entries under the base DN
    • - /// SCOPE_SUB - searches the base DN and all entries - /// within its subtree - ///
    - /// The search filter specifying the search criteria. - /// The names of attributes to retrieve. - /// operation exceeds the time limit. - /// Specifies when aliases should be dereferenced. - /// Must be one of the constants defined in - /// LdapConstraints, which are DEREF_NEVER, - /// DEREF_FINDING, DEREF_SEARCHING, or DEREF_ALWAYS. - /// The maximum number of search results to return - /// for a search request. - /// The search operation will be terminated by the server - /// with an LdapException.SIZE_LIMIT_EXCEEDED if the - /// number of results exceed the maximum. - /// The maximum time in seconds that the server - /// should spend returning search results. This is a - /// server-enforced limit. A value of 0 means - /// no time limit. - /// If true, returns the names but not the values of - /// the attributes found. If false, returns the - /// names and values for attributes found. - /// Any controls that apply to the search request. - /// or null if none. - /// - public LdapSearchRequest( - string ldapBase, - LdapScope scope, - string filter, - string[] attrs, - int dereference, - int maxResults, - int serverTimeLimit, - bool typesOnly, - LdapControl[] cont) - : base( - LdapOperation.SearchRequest, - new RfcSearchRequest(ldapBase, scope, dereference, maxResults, serverTimeLimit, typesOnly, filter, attrs), - cont) - { - } - - /// - /// Retrieves an Iterator object representing the parsed filter for - /// this search request. - /// The first object returned from the Iterator is an Integer indicating - /// the type of filter component. One or more values follow the component - /// type as subsequent items in the Iterator. The pattern of Integer - /// component type followed by values continues until the end of the - /// filter. - /// Values returned as a byte array may represent UTF-8 characters or may - /// be binary values. The possible Integer components of a search filter - /// and the associated values that follow are:. - ///
    • AND - followed by an Iterator value
    • OR - followed by an Iterator value
    • NOT - followed by an Iterator value
    • - /// EQUALITY_MATCH - followed by the attribute name represented as a - /// String, and by the attribute value represented as a byte array - ///
    • - /// GREATER_OR_EQUAL - followed by the attribute name represented as a - /// String, and by the attribute value represented as a byte array - ///
    • - /// LESS_OR_EQUAL - followed by the attribute name represented as a - /// String, and by the attribute value represented as a byte array - ///
    • - /// APPROX_MATCH - followed by the attribute name represented as a - /// String, and by the attribute value represented as a byte array - ///
    • PRESENT - followed by a attribute name respresented as a String
    • - /// EXTENSIBLE_MATCH - followed by the name of the matching rule - /// represented as a String, by the attribute name represented - /// as a String, and by the attribute value represented as a - /// byte array. - ///
    • - /// SUBSTRINGS - followed by the attribute name represented as a - /// String, by one or more SUBSTRING components (INITIAL, ANY, - /// or FINAL) followed by the SUBSTRING value. - ///
    - ///
    - /// - /// The search filter. - /// - public IEnumerator SearchFilter => RfcFilter.GetFilterIterator(); - - /// - /// Retrieves the Base DN for a search request. - /// - /// - /// the base DN for a search request. - /// - public string DN => Asn1Object.RequestDn; - - /// - /// Retrieves the scope of a search request. - /// - /// - /// The scope. - /// - public int Scope => ((Asn1Enumerated)((RfcSearchRequest)Asn1Object.Get(1)).Get(1)).IntValue(); - - /// - /// Retrieves the behaviour of dereferencing aliases on a search request. - /// - /// - /// The dereference. - /// - public int Dereference => ((Asn1Enumerated)((RfcSearchRequest)Asn1Object.Get(1)).Get(2)).IntValue(); - - /// - /// Retrieves the maximum number of entries to be returned on a search. - /// - /// - /// The maximum results. - /// - public int MaxResults => ((Asn1Integer)((RfcSearchRequest)Asn1Object.Get(1)).Get(3)).IntValue(); - - /// - /// Retrieves the server time limit for a search request. - /// - /// - /// The server time limit. - /// - public int ServerTimeLimit => ((Asn1Integer)((RfcSearchRequest)Asn1Object.Get(1)).Get(4)).IntValue(); - - /// - /// Retrieves whether attribute values or only attribute types(names) should - /// be returned in a search request. - /// - /// - /// true if [types only]; otherwise, false. - /// - public bool TypesOnly => ((Asn1Boolean)((RfcSearchRequest)Asn1Object.Get(1)).Get(5)).BooleanValue(); - - /// - /// Retrieves an array of attribute names to request for in a search. - /// - /// - /// The attributes. - /// - public string[] Attributes - { - get - { - var attrs = (RfcAttributeDescriptionList)((RfcSearchRequest)Asn1Object.Get(1)).Get(7); - var values = new string[attrs.Size()]; - for (var i = 0; i < values.Length; i++) - { - values[i] = ((Asn1OctetString)attrs.Get(i)).StringValue(); - } - - return values; - } - } - - /// - /// Creates a string representation of the filter in this search request. - /// - /// - /// The string filter. - /// - public string StringFilter => RfcFilter.FilterToString(); - - /// - /// Retrieves an SearchFilter object representing a filter for a search request. - /// - /// - /// The RFC filter. - /// - private RfcFilter RfcFilter => (RfcFilter)((RfcSearchRequest)Asn1Object.Get(1)).Get(6); - } + /// The base distinguished name to search from. + /// The scope of the entries to search. The following + /// are the valid options:. + ///
    • SCOPE_BASE - searches only the base DN
    • SCOPE_ONE - searches only entries under the base DN
    • + /// SCOPE_SUB - searches the base DN and all entries + /// within its subtree + ///
    + /// The search filter specifying the search criteria. + /// The names of attributes to retrieve. + /// operation exceeds the time limit. + /// Specifies when aliases should be dereferenced. + /// Must be one of the constants defined in + /// LdapConstraints, which are DEREF_NEVER, + /// DEREF_FINDING, DEREF_SEARCHING, or DEREF_ALWAYS. + /// The maximum number of search results to return + /// for a search request. + /// The search operation will be terminated by the server + /// with an LdapException.SIZE_LIMIT_EXCEEDED if the + /// number of results exceed the maximum. + /// The maximum time in seconds that the server + /// should spend returning search results. This is a + /// server-enforced limit. A value of 0 means + /// no time limit. + /// If true, returns the names but not the values of + /// the attributes found. If false, returns the + /// names and values for attributes found. + /// Any controls that apply to the search request. + /// or null if none. + /// + public LdapSearchRequest( + String ldapBase, + LdapScope scope, + String filter, + String[] attrs, + Int32 dereference, + Int32 maxResults, + Int32 serverTimeLimit, + Boolean typesOnly, + LdapControl[] cont) + : base( + LdapOperation.SearchRequest, + new RfcSearchRequest(ldapBase, scope, dereference, maxResults, serverTimeLimit, typesOnly, filter, attrs), + cont) { + } + + /// + /// Retrieves an Iterator object representing the parsed filter for + /// this search request. + /// The first object returned from the Iterator is an Integer indicating + /// the type of filter component. One or more values follow the component + /// type as subsequent items in the Iterator. The pattern of Integer + /// component type followed by values continues until the end of the + /// filter. + /// Values returned as a byte array may represent UTF-8 characters or may + /// be binary values. The possible Integer components of a search filter + /// and the associated values that follow are:. + ///
    • AND - followed by an Iterator value
    • OR - followed by an Iterator value
    • NOT - followed by an Iterator value
    • + /// EQUALITY_MATCH - followed by the attribute name represented as a + /// String, and by the attribute value represented as a byte array + ///
    • + /// GREATER_OR_EQUAL - followed by the attribute name represented as a + /// String, and by the attribute value represented as a byte array + ///
    • + /// LESS_OR_EQUAL - followed by the attribute name represented as a + /// String, and by the attribute value represented as a byte array + ///
    • + /// APPROX_MATCH - followed by the attribute name represented as a + /// String, and by the attribute value represented as a byte array + ///
    • PRESENT - followed by a attribute name respresented as a String
    • + /// EXTENSIBLE_MATCH - followed by the name of the matching rule + /// represented as a String, by the attribute name represented + /// as a String, and by the attribute value represented as a + /// byte array. + ///
    • + /// SUBSTRINGS - followed by the attribute name represented as a + /// String, by one or more SUBSTRING components (INITIAL, ANY, + /// or FINAL) followed by the SUBSTRING value. + ///
    + ///
    + /// + /// The search filter. + /// + public IEnumerator SearchFilter => this.RfcFilter.GetFilterIterator(); + + /// + /// Retrieves the Base DN for a search request. + /// + /// + /// the base DN for a search request. + /// + public String DN => this.Asn1Object.RequestDn; + + /// + /// Retrieves the scope of a search request. + /// + /// + /// The scope. + /// + public Int32 Scope => ((Asn1Enumerated)((RfcSearchRequest)this.Asn1Object.Get(1)).Get(1)).IntValue(); + + /// + /// Retrieves the behaviour of dereferencing aliases on a search request. + /// + /// + /// The dereference. + /// + public Int32 Dereference => ((Asn1Enumerated)((RfcSearchRequest)this.Asn1Object.Get(1)).Get(2)).IntValue(); + + /// + /// Retrieves the maximum number of entries to be returned on a search. + /// + /// + /// The maximum results. + /// + public Int32 MaxResults => ((Asn1Integer)((RfcSearchRequest)this.Asn1Object.Get(1)).Get(3)).IntValue(); + + /// + /// Retrieves the server time limit for a search request. + /// + /// + /// The server time limit. + /// + public Int32 ServerTimeLimit => ((Asn1Integer)((RfcSearchRequest)this.Asn1Object.Get(1)).Get(4)).IntValue(); + + /// + /// Retrieves whether attribute values or only attribute types(names) should + /// be returned in a search request. + /// + /// + /// true if [types only]; otherwise, false. + /// + public Boolean TypesOnly => ((Asn1Boolean)((RfcSearchRequest)this.Asn1Object.Get(1)).Get(5)).BooleanValue(); + + /// + /// Retrieves an array of attribute names to request for in a search. + /// + /// + /// The attributes. + /// + public String[] Attributes { + get { + RfcAttributeDescriptionList attrs = (RfcAttributeDescriptionList)((RfcSearchRequest)this.Asn1Object.Get(1)).Get(7); + String[] values = new String[attrs.Size()]; + for(Int32 i = 0; i < values.Length; i++) { + values[i] = ((Asn1OctetString)attrs.Get(i)).StringValue(); + } + + return values; + } + } + + /// + /// Creates a string representation of the filter in this search request. + /// + /// + /// The string filter. + /// + public String StringFilter => this.RfcFilter.FilterToString(); + + /// + /// Retrieves an SearchFilter object representing a filter for a search request. + /// + /// + /// The RFC filter. + /// + private RfcFilter RfcFilter => (RfcFilter)((RfcSearchRequest)this.Asn1Object.Get(1)).Get(6); + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/Ldap/LdapSearchResults.cs b/Unosquare.Swan/Networking/Ldap/LdapSearchResults.cs index 68fbc19..2c5c302 100644 --- a/Unosquare.Swan/Networking/Ldap/LdapSearchResults.cs +++ b/Unosquare.Swan/Networking/Ldap/LdapSearchResults.cs @@ -1,95 +1,87 @@ -namespace Unosquare.Swan.Networking.Ldap -{ - using System; - using System.Collections.Generic; - using System.Linq; - +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Unosquare.Swan.Networking.Ldap { + /// + /// An LdapSearchResults object is returned from a synchronous search + /// operation. It provides access to all results received during the + /// operation (entries and exceptions). + /// + /// + public sealed class LdapSearchResults { + private readonly List _messages; + private readonly Int32 _messageId; + /// - /// An LdapSearchResults object is returned from a synchronous search - /// operation. It provides access to all results received during the - /// operation (entries and exceptions). + /// Initializes a new instance of the class. /// - /// - public sealed class LdapSearchResults - { - private readonly List _messages; - private readonly int _messageId; - - /// - /// Initializes a new instance of the class. - /// - /// The messages. - /// The message identifier. - internal LdapSearchResults(List messages, int messageId) - { - _messages = messages; - _messageId = messageId; - } - - /// - /// Returns a count of the items in the search result. - /// Returns a count of the entries and exceptions remaining in the object. - /// If the search was submitted with a batch size greater than zero, - /// getCount reports the number of results received so far but not enumerated - /// with next(). If batch size equals zero, getCount reports the number of - /// items received, since the application thread blocks until all results are - /// received. - /// - /// - /// The count. - /// - public int Count => new List(_messages) - .Count(x => x.MessageId == _messageId && GetResponse(x) is LdapSearchResult); - - /// - /// Reports if there are more search results. - /// - /// - /// true if there are more search results. - /// - public bool HasMore() => new List(_messages) - .Any(x => x.MessageId == _messageId && GetResponse(x) is LdapSearchResult); - - /// - /// Returns the next result as an LdapEntry. - /// If automatic referral following is disabled or if a referral - /// was not followed, next() will throw an LdapReferralException - /// when the referral is received. - /// - /// - /// The next search result as an LdapEntry. - /// - /// Next - No more results. - public LdapEntry Next() - { - var list = new List(_messages) - .Where(x => x.MessageId == _messageId); - - foreach (var item in list) - { - _messages.Remove(item); - var response = GetResponse(item); - - if (response is LdapSearchResult result) - { - return result.Entry; - } - } - - throw new ArgumentOutOfRangeException(nameof(Next), "No more results"); - } - - private static LdapMessage GetResponse(RfcLdapMessage item) - { - switch (item.Type) - { - case LdapOperation.SearchResponse: - return new LdapSearchResult(item); - case LdapOperation.SearchResultReference: - return new LdapSearchResultReference(item); - default: - return new LdapResponse(item); - } - } - } + /// The messages. + /// The message identifier. + internal LdapSearchResults(List messages, Int32 messageId) { + this._messages = messages; + this._messageId = messageId; + } + + /// + /// Returns a count of the items in the search result. + /// Returns a count of the entries and exceptions remaining in the object. + /// If the search was submitted with a batch size greater than zero, + /// getCount reports the number of results received so far but not enumerated + /// with next(). If batch size equals zero, getCount reports the number of + /// items received, since the application thread blocks until all results are + /// received. + /// + /// + /// The count. + /// + public Int32 Count => new List(this._messages) + .Count(x => x.MessageId == this._messageId && GetResponse(x) is LdapSearchResult); + + /// + /// Reports if there are more search results. + /// + /// + /// true if there are more search results. + /// + public Boolean HasMore() => new List(this._messages) + .Any(x => x.MessageId == this._messageId && GetResponse(x) is LdapSearchResult); + + /// + /// Returns the next result as an LdapEntry. + /// If automatic referral following is disabled or if a referral + /// was not followed, next() will throw an LdapReferralException + /// when the referral is received. + /// + /// + /// The next search result as an LdapEntry. + /// + /// Next - No more results. + public LdapEntry Next() { + IEnumerable list = new List(this._messages) + .Where(x => x.MessageId == this._messageId); + + foreach(RfcLdapMessage item in list) { + _ = this._messages.Remove(item); + LdapMessage response = GetResponse(item); + + if(response is LdapSearchResult result) { + return result.Entry; + } + } + + throw new ArgumentOutOfRangeException(nameof(Next), "No more results"); + } + + private static LdapMessage GetResponse(RfcLdapMessage item) { + switch(item.Type) { + case LdapOperation.SearchResponse: + return new LdapSearchResult(item); + case LdapOperation.SearchResultReference: + return new LdapSearchResultReference(item); + default: + return new LdapResponse(item); + } + } + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/Ldap/LdapStatusCode.cs b/Unosquare.Swan/Networking/Ldap/LdapStatusCode.cs index 897e45c..ebb2201 100644 --- a/Unosquare.Swan/Networking/Ldap/LdapStatusCode.cs +++ b/Unosquare.Swan/Networking/Ldap/LdapStatusCode.cs @@ -1,539 +1,537 @@ -namespace Unosquare.Swan.Networking.Ldap -{ +namespace Unosquare.Swan.Networking.Ldap { + /// + /// LDAP Connection Status Code. + /// + public enum LdapStatusCode { /// - /// LDAP Connection Status Code. + /// Indicates the requested client operation completed successfully. + /// SUCCESS = 0

    ///

    - public enum LdapStatusCode - { - /// - /// Indicates the requested client operation completed successfully. - /// SUCCESS = 0

    - ///

    - Success = 0, - - /// - /// Indicates an internal error. - /// The server is unable to respond with a more specific error and is - /// also unable to properly respond to a request. It does not indicate - /// that the client has sent an erroneous message. - /// OPERATIONS_ERROR = 1 - /// - OperationsError = 1, - - /// - /// Indicates that the server has received an invalid or malformed request - /// from the client. - /// PROTOCOL_ERROR = 2 - /// - ProtocolError = 2, - - /// - /// Indicates that the operation's time limit specified by either the - /// client or the server has been exceeded. - /// On search operations, incomplete results are returned. - /// TIME_LIMIT_EXCEEDED = 3 - /// - TimeLimitExceeded = 3, - - /// - /// Indicates that in a search operation, the size limit specified by - /// the client or the server has been exceeded. Incomplete results are - /// returned. - /// SIZE_LIMIT_EXCEEDED = 4 - /// - SizeLimitExceeded = 4, - - /// - /// Does not indicate an error condition. Indicates that the results of - /// a compare operation are false. - /// COMPARE_FALSE = 5 - /// - CompareFalse = 5, - - /// - /// Does not indicate an error condition. Indicates that the results of a - /// compare operation are true. - /// COMPARE_TRUE = 6 - /// - CompareTrue = 6, - - /// - /// Indicates that during a bind operation the client requested an - /// authentication method not supported by the Ldap server. - /// AUTH_METHOD_NOT_SUPPORTED = 7 - /// - AuthMethodNotSupported = 7, - - /// - /// Indicates a problem with the level of authentication. - /// One of the following has occurred: - ///
    • - /// In bind requests, the Ldap server accepts only strong - /// authentication. - ///
    • - /// In a client request, the client requested an operation such as delete - /// that requires strong authentication. - ///
    • - /// In an unsolicited notice of disconnection, the Ldap server discovers - /// the security protecting the communication between the client and - /// server has unexpectedly failed or been compromised. - ///
    - /// STRONG_AUTH_REQUIRED = 8 - ///
    - StrongAuthRequired = 8, - - /// - /// Returned by some Ldap servers to Ldapv2 clients to indicate that a referral - /// has been returned in the error string. - /// Ldap_PARTIAL_RESULTS = 9 - /// - LdapPartialResults = 9, - - /// - /// Does not indicate an error condition. In Ldapv3, indicates that the server - /// does not hold the target entry of the request, but that the servers in the - /// referral field may. - /// REFERRAL = 10 - /// - Referral = 10, - - /// - /// Indicates that an Ldap server limit set by an administrative authority - /// has been exceeded. - /// ADMIN_LIMIT_EXCEEDED = 11 - /// - AdminLimitExceeded = 11, - - /// - /// Indicates that the Ldap server was unable to satisfy a request because - /// one or more critical extensions were not available. - /// Either the server does not support the control or the control is not - /// appropriate for the operation type. - /// UNAVAILABLE_CRITICAL_EXTENSION = 12 - /// - UnavailableCriticalExtension = 12, - - /// - /// Indicates that the session is not protected by a protocol such as - /// Transport Layer Security (TLS), which provides session confidentiality. - /// CONFIDENTIALITY_REQUIRED = 13 - /// - ConfidentialityRequired = 13, - - /// - /// Does not indicate an error condition, but indicates that the server is - /// ready for the next step in the process. The client must send the server - /// the same SASL mechanism to continue the process. - /// SASL_BIND_IN_PROGRESS = 14 - /// - SaslBindInProgress = 14, - - /// - /// Indicates that the attribute specified in the modify or compare - /// operation does not exist in the entry. - /// NO_SUCH_ATTRIBUTE = 16 - /// - NoSuchAttribute = 16, - - /// - /// Indicates that the attribute specified in the modify or add operation - /// does not exist in the Ldap server's schema. - /// UNDEFINED_ATTRIBUTE_TYPE = 17 - /// - UndefinedAttributeType = 17, - - /// - /// Indicates that the matching rule specified in the search filter does - /// not match a rule defined for the attribute's syntax. - /// INAPPROPRIATE_MATCHING = 18 - /// - InappropriateMatching = 18, - - /// - /// Indicates that the attribute value specified in a modify, add, or - /// modify DN operation violates constraints placed on the attribute. The - /// constraint can be one of size or content (for example, string only, - /// no binary data). - /// CONSTRAINT_VIOLATION = 19 - /// - ConstraintViolation = 19, - - /// - /// Indicates that the attribute value specified in a modify or add - /// operation already exists as a value for that attribute. - /// ATTRIBUTE_OR_VALUE_EXISTS = 20 - /// - AttributeOrValueExists = 20, - - /// - /// Indicates that the attribute value specified in an add, compare, or - /// modify operation is an unrecognized or invalid syntax for the attribute. - /// INVALID_ATTRIBUTE_SYNTAX = 21 - /// - InvalidAttributeSyntax = 21, - - /// - /// Indicates the target object cannot be found. - /// This code is not returned on the following operations: - ///
      - ///
    • - /// Search operations that find the search base but cannot find any - /// entries that match the search filter. - ///
    • - ///
    • Bind operations.
    • - ///
    - /// NO_SUCH_OBJECT = 32 - ///
    - NoSuchObject = 32, - - /// - /// Indicates that an error occurred when an alias was dereferenced. - /// ALIAS_PROBLEM = 33 - /// - AliasProblem = 33, - - /// - /// Indicates that the syntax of the DN is incorrect. - /// If the DN syntax is correct, but the Ldap server's structure - /// rules do not permit the operation, the server returns - /// Ldap_UNWILLING_TO_PERFORM. - /// INVALID_DN_SYNTAX = 34 - /// - InvalidDnSyntax = 34, - - /// - /// Indicates that the specified operation cannot be performed on a - /// leaf entry. - /// This code is not currently in the Ldap specifications, but is - /// reserved for this constant. - /// IS_LEAF = 35 - /// - IsLeaf = 35, - - /// - /// Indicates that during a search operation, either the client does not - /// have access rights to read the aliased object's name or dereferencing - /// is not allowed. - /// ALIAS_DEREFERENCING_PROBLEM = 36 - /// - AliasDereferencingProblem = 36, - - /// - /// Indicates that during a bind operation, the client is attempting to use - /// an authentication method that the client cannot use correctly. - /// For example, either of the following cause this error: - ///
      - ///
    • - /// The client returns simple credentials when strong credentials are - /// required. - ///
    • - ///
    • - /// The client returns a DN and a password for a simple bind when the - /// entry does not have a password defined. - ///
    • - ///
    - /// INAPPROPRIATE_AUTHENTICATION = 48 - ///
    - InappropriateAuthentication = 48, - - /// - /// Indicates that invalid information was passed during a bind operation. - /// One of the following occurred: - ///
      - ///
    • The client passed either an incorrect DN or password.
    • - ///
    • - /// The password is incorrect because it has expired, intruder detection - /// has locked the account, or some other similar reason. - ///
    • - ///
    - /// INVALID_CREDENTIALS = 49 - ///
    - InvalidCredentials = 49, - - /// - /// Indicates that the caller does not have sufficient rights to perform - /// the requested operation. - /// INSUFFICIENT_ACCESS_RIGHTS = 50 - /// - InsufficientAccessRights = 50, - - /// - /// Indicates that the Ldap server is too busy to process the client request - /// at this time, but if the client waits and resubmits the request, the - /// server may be able to process it then. - /// BUSY = 51 - /// - Busy = 51, - - /// - /// Indicates that the Ldap server cannot process the client's bind - /// request, usually because it is shutting down. - /// UNAVAILABLE = 52 - /// - Unavailable = 52, - - /// - /// Indicates that the Ldap server cannot process the request because of - /// server-defined restrictions. - /// This error is returned for the following reasons: - ///
      - ///
    • The add entry request violates the server's structure rules.
    • - ///
    • - /// The modify attribute request specifies attributes that users - /// cannot modify. - ///
    • - ///
    - /// UNWILLING_TO_PERFORM = 53 - ///
    - UnwillingToPerform = 53, - - /// - /// Indicates that the client discovered an alias or referral loop, - /// and is thus unable to complete this request. - /// LOOP_DETECT = 54 - /// - LoopDetect = 54, - - /// - /// Indicates that the add or modify DN operation violates the schema's - /// structure rules. - /// For example, - ///
      - ///
    • The request places the entry subordinate to an alias.
    • - ///
    • - /// The request places the entry subordinate to a container that - /// is forbidden by the containment rules. - ///
    • - ///
    • The RDN for the entry uses a forbidden attribute type.
    • - ///
    - /// NAMING_VIOLATION = 64 - ///
    - NamingViolation = 64, - - /// - /// Indicates that the add, modify, or modify DN operation violates the - /// object class rules for the entry. - /// For example, the following types of request return this error: - ///
      - ///
    • - /// The add or modify operation tries to add an entry without a value - /// for a required attribute. - ///
    • - ///
    • - /// The add or modify operation tries to add an entry with a value for - /// an attribute which the class definition does not contain. - ///
    • - ///
    • - /// The modify operation tries to remove a required attribute without - /// removing the auxiliary class that defines the attribute as required. - ///
    • - ///
    - /// OBJECT_CLASS_VIOLATION = 65 - ///
    - ObjectClassViolation = 65, - - /// - /// Indicates that the requested operation is permitted only on leaf entries. - /// For example, the following types of requests return this error: - ///
      - ///
    • The client requests a delete operation on a parent entry.
    • - ///
    • The client request a modify DN operation on a parent entry.
    • - ///
    - /// NOT_ALLOWED_ON_NONLEAF = 66 - ///
    - NotAllowedOnNonleaf = 66, - - /// - /// Indicates that the modify operation attempted to remove an attribute - /// value that forms the entry's relative distinguished name. - /// NOT_ALLOWED_ON_RDN = 67 - /// - NotAllowedOnRdn = 67, - - /// - /// Indicates that the add operation attempted to add an entry that already - /// exists, or that the modify operation attempted to rename an entry to the - /// name of an entry that already exists. - /// ENTRY_ALREADY_EXISTS = 68 - /// - EntryAlreadyExists = 68, - - /// - /// Indicates that the modify operation attempted to modify the structure - /// rules of an object class. - /// OBJECT_CLASS_MODS_PROHIBITED = 69 - /// - ObjectClassModsProhibited = 69, - - /// - /// Indicates that the modify DN operation moves the entry from one Ldap - /// server to another and thus requires more than one Ldap server. - /// AFFECTS_MULTIPLE_DSAS = 71 - /// - AffectsMultipleDsas = 71, - - /// - /// Indicates an unknown error condition. - /// OTHER = 80 - /// - Other = 80, - - ///////////////////////////////////////////////////////////////////////////// - // Local Errors, resulting from actions other than an operation on a server - ///////////////////////////////////////////////////////////////////////////// - - /// - /// Indicates that the Ldap libraries cannot establish an initial connection - /// with the Ldap server. Either the Ldap server is down or the specified - /// host name or port number is incorrect. - /// SERVER_DOWN = 81 - /// - ServerDown = 81, - - /// - /// Indicates that the Ldap client has an error. This is usually a failed - /// dynamic memory allocation error. - /// LOCAL_ERROR = 82 - /// - LocalError = 82, - - /// - /// Indicates that the Ldap client encountered errors when encoding an - /// Ldap request intended for the Ldap server. - /// ENCODING_ERROR = 83 - /// - EncodingError = 83, - - /// - /// Indicates that the Ldap client encountered errors when decoding an - /// Ldap response from the Ldap server. - /// DECODING_ERROR = 84 - /// - DecodingError = 84, - - /// - /// Indicates that the time limit of the Ldap client was exceeded while - /// waiting for a result. - /// Ldap_TIMEOUT = 85 - /// - LdapTimeout = 85, - - /// - /// Indicates that a bind method was called with an unknown - /// authentication method. - /// AUTH_UNKNOWN = 86 - /// - AuthUnknown = 86, - - /// - /// Indicates that the search method was called with an invalid - /// search filter. - /// FILTER_ERROR = 87 - /// - FilterError = 87, - - /// - /// Indicates that the user cancelled the Ldap operation. - /// USER_CANCELLED = 88 - /// - UserCancelled = 88, - - /// - /// Indicates that a dynamic memory allocation method failed when calling - /// an Ldap method. - /// NO_MEMORY = 90 - /// - NoMemory = 90, - - /// - /// Indicates that the Ldap client has lost either its connection or - /// cannot establish a connection to the Ldap server. - /// CONNECT_ERROR = 91 - /// - ConnectError = 91, - - /// - /// Indicates that the requested functionality is not supported by the - /// client. For example, if the Ldap client is established as an Ldapv2 - /// client, the libraries set this error code when the client requests - /// Ldapv3 functionality. - /// Ldap_NOT_SUPPORTED = 92 - /// - LdapNotSupported = 92, - - /// - /// Indicates that the client requested a control that the libraries - /// cannot find in the list of supported controls sent by the Ldap server. - /// CONTROL_NOT_FOUND = 93 - /// - ControlNotFound = 93, - - /// - /// Indicates that the Ldap server sent no results. - /// NO_RESULTS_RETURNED = 94 - /// - NoResultsReturned = 94, - - /// - /// Indicates that more results are chained in the result message. - /// MORE_RESULTS_TO_RETURN = 95 - /// - MoreResultsToReturn = 95, - - /// - /// Indicates the Ldap libraries detected a loop. Usually this happens - /// when following referrals. - /// CLIENT_LOOP = 96 - /// - ClientLoop = 96, - - /// - /// Indicates that the referral exceeds the hop limit. The default hop - /// limit is ten. - /// The hop limit determines how many servers the client can hop through - /// to retrieve data. For example, suppose the following conditions: - ///
      - ///
    • Suppose the hop limit is two.
    • - ///
    • - /// If the referral is to server D which can be contacted only through - /// server B (1 hop) which contacts server C (2 hops) which contacts - /// server D (3 hops). - ///
    • - ///
    - /// With these conditions, the hop limit is exceeded and the Ldap - /// libraries set this code. - /// REFERRAL_LIMIT_EXCEEDED = 97 - ///
    - ReferralLimitExceeded = 97, - - /// - /// Indicates that the server response to a request is invalid. - /// INVALID_RESPONSE = 100 - /// - InvalidResponse = 100, - - /// - /// Indicates that the server response to a request is ambiguous. - /// AMBIGUOUS_RESPONSE = 101 - /// - AmbiguousResponse = 101, - - /// - /// Indicates that TLS is not supported on the server. - /// TLS_NOT_SUPPORTED = 112 - /// - TlsNotSupported = 112, - - /// - /// Indicates that SSL Handshake could not succeed. - /// SSL_HANDSHAKE_FAILED = 113 - /// - SslHandshakeFailed = 113, - - /// - /// Indicates that SSL Provider could not be found. - /// SSL_PROVIDER_NOT_FOUND = 114 - /// - SslProviderNotFound = 114, - } + Success = 0, + + /// + /// Indicates an internal error. + /// The server is unable to respond with a more specific error and is + /// also unable to properly respond to a request. It does not indicate + /// that the client has sent an erroneous message. + /// OPERATIONS_ERROR = 1 + /// + OperationsError = 1, + + /// + /// Indicates that the server has received an invalid or malformed request + /// from the client. + /// PROTOCOL_ERROR = 2 + /// + ProtocolError = 2, + + /// + /// Indicates that the operation's time limit specified by either the + /// client or the server has been exceeded. + /// On search operations, incomplete results are returned. + /// TIME_LIMIT_EXCEEDED = 3 + /// + TimeLimitExceeded = 3, + + /// + /// Indicates that in a search operation, the size limit specified by + /// the client or the server has been exceeded. Incomplete results are + /// returned. + /// SIZE_LIMIT_EXCEEDED = 4 + /// + SizeLimitExceeded = 4, + + /// + /// Does not indicate an error condition. Indicates that the results of + /// a compare operation are false. + /// COMPARE_FALSE = 5 + /// + CompareFalse = 5, + + /// + /// Does not indicate an error condition. Indicates that the results of a + /// compare operation are true. + /// COMPARE_TRUE = 6 + /// + CompareTrue = 6, + + /// + /// Indicates that during a bind operation the client requested an + /// authentication method not supported by the Ldap server. + /// AUTH_METHOD_NOT_SUPPORTED = 7 + /// + AuthMethodNotSupported = 7, + + /// + /// Indicates a problem with the level of authentication. + /// One of the following has occurred: + ///
    • + /// In bind requests, the Ldap server accepts only strong + /// authentication. + ///
    • + /// In a client request, the client requested an operation such as delete + /// that requires strong authentication. + ///
    • + /// In an unsolicited notice of disconnection, the Ldap server discovers + /// the security protecting the communication between the client and + /// server has unexpectedly failed or been compromised. + ///
    + /// STRONG_AUTH_REQUIRED = 8 + ///
    + StrongAuthRequired = 8, + + /// + /// Returned by some Ldap servers to Ldapv2 clients to indicate that a referral + /// has been returned in the error string. + /// Ldap_PARTIAL_RESULTS = 9 + /// + LdapPartialResults = 9, + + /// + /// Does not indicate an error condition. In Ldapv3, indicates that the server + /// does not hold the target entry of the request, but that the servers in the + /// referral field may. + /// REFERRAL = 10 + /// + Referral = 10, + + /// + /// Indicates that an Ldap server limit set by an administrative authority + /// has been exceeded. + /// ADMIN_LIMIT_EXCEEDED = 11 + /// + AdminLimitExceeded = 11, + + /// + /// Indicates that the Ldap server was unable to satisfy a request because + /// one or more critical extensions were not available. + /// Either the server does not support the control or the control is not + /// appropriate for the operation type. + /// UNAVAILABLE_CRITICAL_EXTENSION = 12 + /// + UnavailableCriticalExtension = 12, + + /// + /// Indicates that the session is not protected by a protocol such as + /// Transport Layer Security (TLS), which provides session confidentiality. + /// CONFIDENTIALITY_REQUIRED = 13 + /// + ConfidentialityRequired = 13, + + /// + /// Does not indicate an error condition, but indicates that the server is + /// ready for the next step in the process. The client must send the server + /// the same SASL mechanism to continue the process. + /// SASL_BIND_IN_PROGRESS = 14 + /// + SaslBindInProgress = 14, + + /// + /// Indicates that the attribute specified in the modify or compare + /// operation does not exist in the entry. + /// NO_SUCH_ATTRIBUTE = 16 + /// + NoSuchAttribute = 16, + + /// + /// Indicates that the attribute specified in the modify or add operation + /// does not exist in the Ldap server's schema. + /// UNDEFINED_ATTRIBUTE_TYPE = 17 + /// + UndefinedAttributeType = 17, + + /// + /// Indicates that the matching rule specified in the search filter does + /// not match a rule defined for the attribute's syntax. + /// INAPPROPRIATE_MATCHING = 18 + /// + InappropriateMatching = 18, + + /// + /// Indicates that the attribute value specified in a modify, add, or + /// modify DN operation violates constraints placed on the attribute. The + /// constraint can be one of size or content (for example, string only, + /// no binary data). + /// CONSTRAINT_VIOLATION = 19 + /// + ConstraintViolation = 19, + + /// + /// Indicates that the attribute value specified in a modify or add + /// operation already exists as a value for that attribute. + /// ATTRIBUTE_OR_VALUE_EXISTS = 20 + /// + AttributeOrValueExists = 20, + + /// + /// Indicates that the attribute value specified in an add, compare, or + /// modify operation is an unrecognized or invalid syntax for the attribute. + /// INVALID_ATTRIBUTE_SYNTAX = 21 + /// + InvalidAttributeSyntax = 21, + + /// + /// Indicates the target object cannot be found. + /// This code is not returned on the following operations: + ///
      + ///
    • + /// Search operations that find the search base but cannot find any + /// entries that match the search filter. + ///
    • + ///
    • Bind operations.
    • + ///
    + /// NO_SUCH_OBJECT = 32 + ///
    + NoSuchObject = 32, + + /// + /// Indicates that an error occurred when an alias was dereferenced. + /// ALIAS_PROBLEM = 33 + /// + AliasProblem = 33, + + /// + /// Indicates that the syntax of the DN is incorrect. + /// If the DN syntax is correct, but the Ldap server's structure + /// rules do not permit the operation, the server returns + /// Ldap_UNWILLING_TO_PERFORM. + /// INVALID_DN_SYNTAX = 34 + /// + InvalidDnSyntax = 34, + + /// + /// Indicates that the specified operation cannot be performed on a + /// leaf entry. + /// This code is not currently in the Ldap specifications, but is + /// reserved for this constant. + /// IS_LEAF = 35 + /// + IsLeaf = 35, + + /// + /// Indicates that during a search operation, either the client does not + /// have access rights to read the aliased object's name or dereferencing + /// is not allowed. + /// ALIAS_DEREFERENCING_PROBLEM = 36 + /// + AliasDereferencingProblem = 36, + + /// + /// Indicates that during a bind operation, the client is attempting to use + /// an authentication method that the client cannot use correctly. + /// For example, either of the following cause this error: + ///
      + ///
    • + /// The client returns simple credentials when strong credentials are + /// required. + ///
    • + ///
    • + /// The client returns a DN and a password for a simple bind when the + /// entry does not have a password defined. + ///
    • + ///
    + /// INAPPROPRIATE_AUTHENTICATION = 48 + ///
    + InappropriateAuthentication = 48, + + /// + /// Indicates that invalid information was passed during a bind operation. + /// One of the following occurred: + ///
      + ///
    • The client passed either an incorrect DN or password.
    • + ///
    • + /// The password is incorrect because it has expired, intruder detection + /// has locked the account, or some other similar reason. + ///
    • + ///
    + /// INVALID_CREDENTIALS = 49 + ///
    + InvalidCredentials = 49, + + /// + /// Indicates that the caller does not have sufficient rights to perform + /// the requested operation. + /// INSUFFICIENT_ACCESS_RIGHTS = 50 + /// + InsufficientAccessRights = 50, + + /// + /// Indicates that the Ldap server is too busy to process the client request + /// at this time, but if the client waits and resubmits the request, the + /// server may be able to process it then. + /// BUSY = 51 + /// + Busy = 51, + + /// + /// Indicates that the Ldap server cannot process the client's bind + /// request, usually because it is shutting down. + /// UNAVAILABLE = 52 + /// + Unavailable = 52, + + /// + /// Indicates that the Ldap server cannot process the request because of + /// server-defined restrictions. + /// This error is returned for the following reasons: + ///
      + ///
    • The add entry request violates the server's structure rules.
    • + ///
    • + /// The modify attribute request specifies attributes that users + /// cannot modify. + ///
    • + ///
    + /// UNWILLING_TO_PERFORM = 53 + ///
    + UnwillingToPerform = 53, + + /// + /// Indicates that the client discovered an alias or referral loop, + /// and is thus unable to complete this request. + /// LOOP_DETECT = 54 + /// + LoopDetect = 54, + + /// + /// Indicates that the add or modify DN operation violates the schema's + /// structure rules. + /// For example, + ///
      + ///
    • The request places the entry subordinate to an alias.
    • + ///
    • + /// The request places the entry subordinate to a container that + /// is forbidden by the containment rules. + ///
    • + ///
    • The RDN for the entry uses a forbidden attribute type.
    • + ///
    + /// NAMING_VIOLATION = 64 + ///
    + NamingViolation = 64, + + /// + /// Indicates that the add, modify, or modify DN operation violates the + /// object class rules for the entry. + /// For example, the following types of request return this error: + ///
      + ///
    • + /// The add or modify operation tries to add an entry without a value + /// for a required attribute. + ///
    • + ///
    • + /// The add or modify operation tries to add an entry with a value for + /// an attribute which the class definition does not contain. + ///
    • + ///
    • + /// The modify operation tries to remove a required attribute without + /// removing the auxiliary class that defines the attribute as required. + ///
    • + ///
    + /// OBJECT_CLASS_VIOLATION = 65 + ///
    + ObjectClassViolation = 65, + + /// + /// Indicates that the requested operation is permitted only on leaf entries. + /// For example, the following types of requests return this error: + ///
      + ///
    • The client requests a delete operation on a parent entry.
    • + ///
    • The client request a modify DN operation on a parent entry.
    • + ///
    + /// NOT_ALLOWED_ON_NONLEAF = 66 + ///
    + NotAllowedOnNonleaf = 66, + + /// + /// Indicates that the modify operation attempted to remove an attribute + /// value that forms the entry's relative distinguished name. + /// NOT_ALLOWED_ON_RDN = 67 + /// + NotAllowedOnRdn = 67, + + /// + /// Indicates that the add operation attempted to add an entry that already + /// exists, or that the modify operation attempted to rename an entry to the + /// name of an entry that already exists. + /// ENTRY_ALREADY_EXISTS = 68 + /// + EntryAlreadyExists = 68, + + /// + /// Indicates that the modify operation attempted to modify the structure + /// rules of an object class. + /// OBJECT_CLASS_MODS_PROHIBITED = 69 + /// + ObjectClassModsProhibited = 69, + + /// + /// Indicates that the modify DN operation moves the entry from one Ldap + /// server to another and thus requires more than one Ldap server. + /// AFFECTS_MULTIPLE_DSAS = 71 + /// + AffectsMultipleDsas = 71, + + /// + /// Indicates an unknown error condition. + /// OTHER = 80 + /// + Other = 80, + + ///////////////////////////////////////////////////////////////////////////// + // Local Errors, resulting from actions other than an operation on a server + ///////////////////////////////////////////////////////////////////////////// + + /// + /// Indicates that the Ldap libraries cannot establish an initial connection + /// with the Ldap server. Either the Ldap server is down or the specified + /// host name or port number is incorrect. + /// SERVER_DOWN = 81 + /// + ServerDown = 81, + + /// + /// Indicates that the Ldap client has an error. This is usually a failed + /// dynamic memory allocation error. + /// LOCAL_ERROR = 82 + /// + LocalError = 82, + + /// + /// Indicates that the Ldap client encountered errors when encoding an + /// Ldap request intended for the Ldap server. + /// ENCODING_ERROR = 83 + /// + EncodingError = 83, + + /// + /// Indicates that the Ldap client encountered errors when decoding an + /// Ldap response from the Ldap server. + /// DECODING_ERROR = 84 + /// + DecodingError = 84, + + /// + /// Indicates that the time limit of the Ldap client was exceeded while + /// waiting for a result. + /// Ldap_TIMEOUT = 85 + /// + LdapTimeout = 85, + + /// + /// Indicates that a bind method was called with an unknown + /// authentication method. + /// AUTH_UNKNOWN = 86 + /// + AuthUnknown = 86, + + /// + /// Indicates that the search method was called with an invalid + /// search filter. + /// FILTER_ERROR = 87 + /// + FilterError = 87, + + /// + /// Indicates that the user cancelled the Ldap operation. + /// USER_CANCELLED = 88 + /// + UserCancelled = 88, + + /// + /// Indicates that a dynamic memory allocation method failed when calling + /// an Ldap method. + /// NO_MEMORY = 90 + /// + NoMemory = 90, + + /// + /// Indicates that the Ldap client has lost either its connection or + /// cannot establish a connection to the Ldap server. + /// CONNECT_ERROR = 91 + /// + ConnectError = 91, + + /// + /// Indicates that the requested functionality is not supported by the + /// client. For example, if the Ldap client is established as an Ldapv2 + /// client, the libraries set this error code when the client requests + /// Ldapv3 functionality. + /// Ldap_NOT_SUPPORTED = 92 + /// + LdapNotSupported = 92, + + /// + /// Indicates that the client requested a control that the libraries + /// cannot find in the list of supported controls sent by the Ldap server. + /// CONTROL_NOT_FOUND = 93 + /// + ControlNotFound = 93, + + /// + /// Indicates that the Ldap server sent no results. + /// NO_RESULTS_RETURNED = 94 + /// + NoResultsReturned = 94, + + /// + /// Indicates that more results are chained in the result message. + /// MORE_RESULTS_TO_RETURN = 95 + /// + MoreResultsToReturn = 95, + + /// + /// Indicates the Ldap libraries detected a loop. Usually this happens + /// when following referrals. + /// CLIENT_LOOP = 96 + /// + ClientLoop = 96, + + /// + /// Indicates that the referral exceeds the hop limit. The default hop + /// limit is ten. + /// The hop limit determines how many servers the client can hop through + /// to retrieve data. For example, suppose the following conditions: + ///
      + ///
    • Suppose the hop limit is two.
    • + ///
    • + /// If the referral is to server D which can be contacted only through + /// server B (1 hop) which contacts server C (2 hops) which contacts + /// server D (3 hops). + ///
    • + ///
    + /// With these conditions, the hop limit is exceeded and the Ldap + /// libraries set this code. + /// REFERRAL_LIMIT_EXCEEDED = 97 + ///
    + ReferralLimitExceeded = 97, + + /// + /// Indicates that the server response to a request is invalid. + /// INVALID_RESPONSE = 100 + /// + InvalidResponse = 100, + + /// + /// Indicates that the server response to a request is ambiguous. + /// AMBIGUOUS_RESPONSE = 101 + /// + AmbiguousResponse = 101, + + /// + /// Indicates that TLS is not supported on the server. + /// TLS_NOT_SUPPORTED = 112 + /// + TlsNotSupported = 112, + + /// + /// Indicates that SSL Handshake could not succeed. + /// SSL_HANDSHAKE_FAILED = 113 + /// + SslHandshakeFailed = 113, + + /// + /// Indicates that SSL Provider could not be found. + /// SSL_PROVIDER_NOT_FOUND = 114 + /// + SslProviderNotFound = 114, + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/Ldap/Message.cs b/Unosquare.Swan/Networking/Ldap/Message.cs index ee19a47..5af7ab3 100644 --- a/Unosquare.Swan/Networking/Ldap/Message.cs +++ b/Unosquare.Swan/Networking/Ldap/Message.cs @@ -1,304 +1,292 @@ -namespace Unosquare.Swan.Networking.Ldap -{ - using System; - using System.Collections.Generic; - +using System; +using System.Collections.Generic; + +namespace Unosquare.Swan.Networking.Ldap { + /// + /// The class performs token processing from strings. + /// + internal class Tokenizer { + // The tokenizer uses the default delimiter set: the space character, the tab character, the newline character, and the carriage-return character + private readonly String _delimiters = " \t\n\r"; + + private readonly Boolean _returnDelims; + + private List _elements; + + private String _source; + /// - /// The class performs token processing from strings. + /// Initializes a new instance of the class. + /// Initializes a new class instance with a specified string to process + /// and the specified token delimiters to use. /// - internal class Tokenizer - { - // The tokenizer uses the default delimiter set: the space character, the tab character, the newline character, and the carriage-return character - private readonly string _delimiters = " \t\n\r"; - - private readonly bool _returnDelims; - - private List _elements; - - private string _source; - - /// - /// Initializes a new instance of the class. - /// Initializes a new class instance with a specified string to process - /// and the specified token delimiters to use. - /// - /// String to tokenize. - /// String containing the delimiters. - /// if set to true [ret delete]. - public Tokenizer(string source, string delimiters, bool retDel = false) - { - _elements = new List(); - _delimiters = delimiters ?? _delimiters; - _source = source; - _returnDelims = retDel; - if (_returnDelims) - Tokenize(); - else - _elements.AddRange(source.Split(_delimiters.ToCharArray())); - RemoveEmptyStrings(); - } - - public int Count => _elements.Count; - - public bool HasMoreTokens() => _elements.Count > 0; - - public string NextToken() - { - if (_source == string.Empty) throw new InvalidOperationException(); - - string result; - if (_returnDelims) - { - RemoveEmptyStrings(); - result = _elements[0]; - _elements.RemoveAt(0); - return result; - } - - _elements = new List(); - _elements.AddRange(_source.Split(_delimiters.ToCharArray())); - RemoveEmptyStrings(); - result = _elements[0]; - _elements.RemoveAt(0); - _source = _source.Remove(_source.IndexOf(result, StringComparison.Ordinal), result.Length); - _source = _source.TrimStart(_delimiters.ToCharArray()); - return result; - } - - private void RemoveEmptyStrings() - { - for (var index = 0; index < _elements.Count; index++) - { - if (_elements[index] != string.Empty) continue; - - _elements.RemoveAt(index); - index--; - } - } - - private void Tokenize() - { - var tempstr = _source; - if (tempstr.IndexOfAny(_delimiters.ToCharArray()) < 0 && tempstr.Length > 0) - { - _elements.Add(tempstr); - } - else if (tempstr.IndexOfAny(_delimiters.ToCharArray()) < 0 && tempstr.Length <= 0) - { - return; - } - - while (tempstr.IndexOfAny(_delimiters.ToCharArray()) >= 0) - { - if (tempstr.IndexOfAny(_delimiters.ToCharArray()) == 0) - { - if (tempstr.Length > 1) - { - _elements.Add(tempstr.Substring(0, 1)); - tempstr = tempstr.Substring(1); - } - else - { - tempstr = string.Empty; - } - } - else - { - var toks = tempstr.Substring(0, tempstr.IndexOfAny(_delimiters.ToCharArray())); - _elements.Add(toks); - _elements.Add(tempstr.Substring(toks.Length, 1)); - - tempstr = tempstr.Length > toks.Length + 1 ? tempstr.Substring(toks.Length + 1) : string.Empty; - } - } - - if (tempstr.Length > 0) - { - _elements.Add(tempstr); - } - } - } - + /// String to tokenize. + /// String containing the delimiters. + /// if set to true [ret delete]. + public Tokenizer(String source, String delimiters, Boolean retDel = false) { + this._elements = new List(); + this._delimiters = delimiters ?? this._delimiters; + this._source = source; + this._returnDelims = retDel; + if(this._returnDelims) { + this.Tokenize(); + } else { + this._elements.AddRange(source.Split(this._delimiters.ToCharArray())); + } + + this.RemoveEmptyStrings(); + } + + public Int32 Count => this._elements.Count; + + public Boolean HasMoreTokens() => this._elements.Count > 0; + + public String NextToken() { + if(this._source == String.Empty) { + throw new InvalidOperationException(); + } + + String result; + if(this._returnDelims) { + this.RemoveEmptyStrings(); + result = this._elements[0]; + this._elements.RemoveAt(0); + return result; + } + + this._elements = new List(); + this._elements.AddRange(this._source.Split(this._delimiters.ToCharArray())); + this.RemoveEmptyStrings(); + result = this._elements[0]; + this._elements.RemoveAt(0); + this._source = this._source.Remove(this._source.IndexOf(result, StringComparison.Ordinal), result.Length); + this._source = this._source.TrimStart(this._delimiters.ToCharArray()); + return result; + } + + private void RemoveEmptyStrings() { + for(Int32 index = 0; index < this._elements.Count; index++) { + if(this._elements[index] != String.Empty) { + continue; + } + + this._elements.RemoveAt(index); + index--; + } + } + + private void Tokenize() { + String tempstr = this._source; + if(tempstr.IndexOfAny(this._delimiters.ToCharArray()) < 0 && tempstr.Length > 0) { + this._elements.Add(tempstr); + } else if(tempstr.IndexOfAny(this._delimiters.ToCharArray()) < 0 && tempstr.Length <= 0) { + return; + } + + while(tempstr.IndexOfAny(this._delimiters.ToCharArray()) >= 0) { + if(tempstr.IndexOfAny(this._delimiters.ToCharArray()) == 0) { + if(tempstr.Length > 1) { + this._elements.Add(tempstr.Substring(0, 1)); + tempstr = tempstr.Substring(1); + } else { + tempstr = String.Empty; + } + } else { + String toks = tempstr.Substring(0, tempstr.IndexOfAny(this._delimiters.ToCharArray())); + this._elements.Add(toks); + this._elements.Add(tempstr.Substring(toks.Length, 1)); + + tempstr = tempstr.Length > toks.Length + 1 ? tempstr.Substring(toks.Length + 1) : String.Empty; + } + } + + if(tempstr.Length > 0) { + this._elements.Add(tempstr); + } + } + } + + /// + /// Represents an Ldap Matching Rule Assertion. + ///
    +  /// MatchingRuleAssertion ::= SEQUENCE {
    +  /// matchingRule    [1] MatchingRuleId OPTIONAL,
    +  /// type            [2] AttributeDescription OPTIONAL,
    +  /// matchValue      [3] AssertionValue,
    +  /// dnAttributes    [4] BOOLEAN DEFAULT FALSE }
    +  /// 
    + /// + internal class RfcMatchingRuleAssertion : Asn1Sequence { + public RfcMatchingRuleAssertion( + String matchingRule, + String type, + SByte[] matchValue, + Asn1Boolean dnAttributes = null) + : base(4) { + if(matchingRule != null) { + this.Add(new Asn1Tagged(new Asn1Identifier(1), new Asn1OctetString(matchingRule), false)); + } + + if(type != null) { + this.Add(new Asn1Tagged(new Asn1Identifier(2), new Asn1OctetString(type), false)); + } + + this.Add(new Asn1Tagged(new Asn1Identifier(3), new Asn1OctetString(matchValue), false)); + + // if dnAttributes if false, that is the default value and we must not + // encode it. (See RFC 2251 5.1 number 4) + if(dnAttributes != null && dnAttributes.BooleanValue()) { + this.Add(new Asn1Tagged(new Asn1Identifier(4), dnAttributes, false)); + } + } + } + + /// + /// The AttributeDescriptionList is used to list attributes to be returned in + /// a search request. + ///
    +  /// AttributeDescriptionList ::= SEQUENCE OF
    +  /// AttributeDescription
    +  /// 
    + /// + internal class RfcAttributeDescriptionList : Asn1SequenceOf { + public RfcAttributeDescriptionList(String[] attrs) + : base(attrs?.Length ?? 0) { + if(attrs == null) { + return; + } + + foreach(String attr in attrs) { + this.Add(attr); + } + } + } + + /// + /// Represents an Ldap Search Request. + ///
    +  /// SearchRequest ::= [APPLICATION 3] SEQUENCE {
    +  /// baseObject      LdapDN,
    +  /// scope           ENUMERATED {
    +  /// baseObject              (0),
    +  /// singleLevel             (1),
    +  /// wholeSubtree            (2) },
    +  /// derefAliases    ENUMERATED {
    +  /// neverDerefAliases       (0),
    +  /// derefInSearching        (1),
    +  /// derefFindingBaseObj     (2),
    +  /// derefAlways             (3) },
    +  /// sizeLimit       INTEGER (0 .. maxInt),
    +  /// timeLimit       INTEGER (0 .. maxInt),
    +  /// typesOnly       BOOLEAN,
    +  /// filter          Filter,
    +  /// attributes      AttributeDescriptionList }
    +  /// 
    + ///
    + /// + /// + internal class RfcSearchRequest : Asn1Sequence, IRfcRequest { + public RfcSearchRequest( + String basePath, + LdapScope scope, + Int32 derefAliases, + Int32 sizeLimit, + Int32 timeLimit, + Boolean typesOnly, + String filter, + String[] attributes) + : base(8) { + this.Add(basePath); + this.Add(new Asn1Enumerated(scope)); + this.Add(new Asn1Enumerated(derefAliases)); + this.Add(new Asn1Integer(sizeLimit)); + this.Add(new Asn1Integer(timeLimit)); + this.Add(new Asn1Boolean(typesOnly)); + this.Add(new RfcFilter(filter)); + this.Add(new RfcAttributeDescriptionList(attributes)); + } + + public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.SearchRequest); + + public String GetRequestDN() => ((Asn1OctetString)this.Get(0)).StringValue(); + } + + /// + /// Represents an Ldap Substring Filter. + ///
    +  /// SubstringFilter ::= SEQUENCE {
    +  /// type            AttributeDescription,
    +  /// -- at least one must be present
    +  /// substrings      SEQUENCE OF CHOICE {
    +  /// initial [0] LdapString,
    +  /// any     [1] LdapString,
    +  /// final   [2] LdapString } }
    +  /// 
    + ///
    + /// + internal class RfcSubstringFilter : Asn1Sequence { + public RfcSubstringFilter(String type, Asn1Object substrings) + : base(2) { + this.Add(type); + this.Add(substrings); + } + } + + /// + /// Represents an Ldap Attribute Value Assertion. + ///
    +  /// AttributeValueAssertion ::= SEQUENCE {
    +  /// attributeDesc   AttributeDescription,
    +  /// assertionValue  AssertionValue }
    +  /// 
    + ///
    + /// + internal class RfcAttributeValueAssertion : Asn1Sequence { + public RfcAttributeValueAssertion(String ad, SByte[] av) + : base(2) { + this.Add(ad); + this.Add(new Asn1OctetString(av)); + } + + public String AttributeDescription => ((Asn1OctetString)this.Get(0)).StringValue(); + + public SByte[] AssertionValue => ((Asn1OctetString)this.Get(1)).ByteValue(); + } + + /// Encapsulates an Ldap Bind properties. + internal class BindProperties { /// - /// Represents an Ldap Matching Rule Assertion. - ///
    -    /// MatchingRuleAssertion ::= SEQUENCE {
    -    /// matchingRule    [1] MatchingRuleId OPTIONAL,
    -    /// type            [2] AttributeDescription OPTIONAL,
    -    /// matchValue      [3] AssertionValue,
    -    /// dnAttributes    [4] BOOLEAN DEFAULT FALSE }
    -    /// 
    - /// - internal class RfcMatchingRuleAssertion : Asn1Sequence - { - public RfcMatchingRuleAssertion( - string matchingRule, - string type, - sbyte[] matchValue, - Asn1Boolean dnAttributes = null) - : base(4) - { - if (matchingRule != null) - Add(new Asn1Tagged(new Asn1Identifier(1), new Asn1OctetString(matchingRule), false)); - if (type != null) - Add(new Asn1Tagged(new Asn1Identifier(2), new Asn1OctetString(type), false)); - - Add(new Asn1Tagged(new Asn1Identifier(3), new Asn1OctetString(matchValue), false)); - - // if dnAttributes if false, that is the default value and we must not - // encode it. (See RFC 2251 5.1 number 4) - if (dnAttributes != null && dnAttributes.BooleanValue()) - Add(new Asn1Tagged(new Asn1Identifier(4), dnAttributes, false)); - } - } - - /// - /// The AttributeDescriptionList is used to list attributes to be returned in - /// a search request. - ///
    -    /// AttributeDescriptionList ::= SEQUENCE OF
    -    /// AttributeDescription
    -    /// 
    - /// - internal class RfcAttributeDescriptionList : Asn1SequenceOf - { - public RfcAttributeDescriptionList(string[] attrs) - : base(attrs?.Length ?? 0) - { - if (attrs == null) return; - - foreach (var attr in attrs) - { - Add(attr); - } - } - } - - /// - /// Represents an Ldap Search Request. - ///
    -    /// SearchRequest ::= [APPLICATION 3] SEQUENCE {
    -    /// baseObject      LdapDN,
    -    /// scope           ENUMERATED {
    -    /// baseObject              (0),
    -    /// singleLevel             (1),
    -    /// wholeSubtree            (2) },
    -    /// derefAliases    ENUMERATED {
    -    /// neverDerefAliases       (0),
    -    /// derefInSearching        (1),
    -    /// derefFindingBaseObj     (2),
    -    /// derefAlways             (3) },
    -    /// sizeLimit       INTEGER (0 .. maxInt),
    -    /// timeLimit       INTEGER (0 .. maxInt),
    -    /// typesOnly       BOOLEAN,
    -    /// filter          Filter,
    -    /// attributes      AttributeDescriptionList }
    -    /// 
    + /// Initializes a new instance of the class. ///
    - /// - /// - internal class RfcSearchRequest : Asn1Sequence, IRfcRequest - { - public RfcSearchRequest( - string basePath, - LdapScope scope, - int derefAliases, - int sizeLimit, - int timeLimit, - bool typesOnly, - string filter, - string[] attributes) - : base(8) - { - Add(basePath); - Add(new Asn1Enumerated(scope)); - Add(new Asn1Enumerated(derefAliases)); - Add(new Asn1Integer(sizeLimit)); - Add(new Asn1Integer(timeLimit)); - Add(new Asn1Boolean(typesOnly)); - Add(new RfcFilter(filter)); - Add(new RfcAttributeDescriptionList(attributes)); - } - - public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.SearchRequest); - - public string GetRequestDN() => ((Asn1OctetString) Get(0)).StringValue(); - } - - /// - /// Represents an Ldap Substring Filter. - ///
    -    /// SubstringFilter ::= SEQUENCE {
    -    /// type            AttributeDescription,
    -    /// -- at least one must be present
    -    /// substrings      SEQUENCE OF CHOICE {
    -    /// initial [0] LdapString,
    -    /// any     [1] LdapString,
    -    /// final   [2] LdapString } }
    -    /// 
    - ///
    - /// - internal class RfcSubstringFilter : Asn1Sequence - { - public RfcSubstringFilter(string type, Asn1Object substrings) - : base(2) - { - Add(type); - Add(substrings); - } - } - - /// - /// Represents an Ldap Attribute Value Assertion. - ///
    -    /// AttributeValueAssertion ::= SEQUENCE {
    -    /// attributeDesc   AttributeDescription,
    -    /// assertionValue  AssertionValue }
    -    /// 
    - ///
    - /// - internal class RfcAttributeValueAssertion : Asn1Sequence - { - public RfcAttributeValueAssertion(string ad, sbyte[] av) - : base(2) - { - Add(ad); - Add(new Asn1OctetString(av)); - } - - public string AttributeDescription => ((Asn1OctetString) Get(0)).StringValue(); - - public sbyte[] AssertionValue => ((Asn1OctetString) Get(1)).ByteValue(); - } - - /// Encapsulates an Ldap Bind properties. - internal class BindProperties - { - /// - /// Initializes a new instance of the class. - /// - /// The version. - /// The dn. - /// The method. - /// if set to true [anonymous]. - public BindProperties( - int version, - string dn, - string method, - bool anonymous) - { - ProtocolVersion = version; - AuthenticationDN = dn; - AuthenticationMethod = method; - Anonymous = anonymous; - } - - public int ProtocolVersion { get; } - - public string AuthenticationDN { get; } - - public string AuthenticationMethod { get; } - - public bool Anonymous { get; } - } + /// The version. + /// The dn. + /// The method. + /// if set to true [anonymous]. + public BindProperties( + Int32 version, + String dn, + String method, + Boolean anonymous) { + this.ProtocolVersion = version; + this.AuthenticationDN = dn; + this.AuthenticationMethod = method; + this.Anonymous = anonymous; + } + + public Int32 ProtocolVersion { + get; + } + + public String AuthenticationDN { + get; + } + + public String AuthenticationMethod { + get; + } + + public Boolean Anonymous { + get; + } + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/Ldap/RfcControl.cs b/Unosquare.Swan/Networking/Ldap/RfcControl.cs index d7eec36..b779c00 100644 --- a/Unosquare.Swan/Networking/Ldap/RfcControl.cs +++ b/Unosquare.Swan/Networking/Ldap/RfcControl.cs @@ -1,131 +1,118 @@ -namespace Unosquare.Swan.Networking.Ldap -{ +using System; +namespace Unosquare.Swan.Networking.Ldap { + /// + /// Represents an Ldap Control. + ///
    +  /// Control ::= SEQUENCE {
    +  /// controlType             LdapOID,
    +  /// criticality             BOOLEAN DEFAULT FALSE,
    +  /// controlValue            OCTET STRING OPTIONAL }
    +  /// 
    + ///
    + /// + internal class RfcControl : Asn1Sequence { /// - /// Represents an Ldap Control. - ///
    -    /// Control ::= SEQUENCE {
    -    /// controlType             LdapOID,
    -    /// criticality             BOOLEAN DEFAULT FALSE,
    -    /// controlValue            OCTET STRING OPTIONAL }
    -    /// 
    + /// Initializes a new instance of the class. + /// Note: criticality is only added if true, as per RFC 2251 sec 5.1 part + /// (4): If a value of a type is its default value, it MUST be + /// absent. ///
    - /// - internal class RfcControl : Asn1Sequence - { - /// - /// Initializes a new instance of the class. - /// Note: criticality is only added if true, as per RFC 2251 sec 5.1 part - /// (4): If a value of a type is its default value, it MUST be - /// absent. - /// - /// Type of the control. - /// The criticality. - /// The control value. - public RfcControl(string controlType, Asn1Boolean criticality = null, Asn1Object controlValue = null) - : base(3) - { - Add(controlType); - Add(criticality ?? new Asn1Boolean(false)); - - if (controlValue != null) - Add(controlValue); - } - - public RfcControl(Asn1Structured seqObj) - : base(3) - { - for (var i = 0; i < seqObj.Size(); i++) - Add(seqObj.Get(i)); - } - - public Asn1OctetString ControlType => (Asn1OctetString)Get(0); - - public Asn1Boolean Criticality => Size() > 1 && Get(1) is Asn1Boolean boolean ? boolean : new Asn1Boolean(false); - - public Asn1OctetString ControlValue - { - get - { - if (Size() > 2) - { - // MUST be a control value - return (Asn1OctetString)Get(2); - } - - return Size() > 1 && Get(1) is Asn1OctetString s ? s : null; - } - set - { - if (value == null) - return; - - if (Size() == 3) - { - // We already have a control value, replace it - Set(2, value); - return; - } - - if (Size() == 2) - { - // Get the second element - var obj = Get(1); - - // Is this a control value - if (obj is Asn1OctetString) - { - // replace this one - Set(1, value); - } - else - { - // add a new one at the end - Add(value); - } - } - } - } - } - - /// - /// Represents Ldap Sasl Credentials. - ///
    -    /// SaslCredentials ::= SEQUENCE {
    -    /// mechanism               LdapString,
    -    /// credentials             OCTET STRING OPTIONAL }
    -    /// 
    - /// - internal class RfcSaslCredentials : Asn1Sequence - { - public RfcSaslCredentials(string mechanism, sbyte[] credentials = null) - : base(2) - { - Add(mechanism); - if (credentials != null) - Add(new Asn1OctetString(credentials)); - } - } - - /// - /// Represents an Ldap Authentication Choice. - ///
    -    /// AuthenticationChoice ::= CHOICE {
    -    /// simple                  [0] OCTET STRING,
    -    /// -- 1 and 2 reserved
    -    /// sasl                    [3] SaslCredentials }
    -    /// 
    - /// - internal class RfcAuthenticationChoice : Asn1Choice - { - public RfcAuthenticationChoice(sbyte[] passwd) - : base(new Asn1Tagged(new Asn1Identifier(0), new Asn1OctetString(passwd), false)) - { - } - - public RfcAuthenticationChoice(string mechanism, sbyte[] credentials) - : base(new Asn1Tagged(new Asn1Identifier(3, true), new RfcSaslCredentials(mechanism, credentials), false)) - { - // implicit tagging - } - } + /// Type of the control. + /// The criticality. + /// The control value. + public RfcControl(String controlType, Asn1Boolean criticality = null, Asn1Object controlValue = null) + : base(3) { + this.Add(controlType); + this.Add(criticality ?? new Asn1Boolean(false)); + + if(controlValue != null) { + this.Add(controlValue); + } + } + + public RfcControl(Asn1Structured seqObj) + : base(3) { + for(Int32 i = 0; i < seqObj.Size(); i++) { + this.Add(seqObj.Get(i)); + } + } + + public Asn1OctetString ControlType => (Asn1OctetString)this.Get(0); + + public Asn1Boolean Criticality => this.Size() > 1 && this.Get(1) is Asn1Boolean boolean ? boolean : new Asn1Boolean(false); + + public Asn1OctetString ControlValue { + get { + if(this.Size() > 2) { + // MUST be a control value + return (Asn1OctetString)this.Get(2); + } + + return this.Size() > 1 && this.Get(1) is Asn1OctetString s ? s : null; + } + set { + if(value == null) { + return; + } + + if(this.Size() == 3) { + // We already have a control value, replace it + this.Set(2, value); + return; + } + + if(this.Size() == 2) { + // Get the second element + Asn1Object obj = this.Get(1); + + // Is this a control value + if(obj is Asn1OctetString) { + // replace this one + this.Set(1, value); + } else { + // add a new one at the end + this.Add(value); + } + } + } + } + } + + /// + /// Represents Ldap Sasl Credentials. + ///
    +  /// SaslCredentials ::= SEQUENCE {
    +  /// mechanism               LdapString,
    +  /// credentials             OCTET STRING OPTIONAL }
    +  /// 
    + /// + internal class RfcSaslCredentials : Asn1Sequence { + public RfcSaslCredentials(String mechanism, SByte[] credentials = null) + : base(2) { + this.Add(mechanism); + if(credentials != null) { + this.Add(new Asn1OctetString(credentials)); + } + } + } + + /// + /// Represents an Ldap Authentication Choice. + ///
    +  /// AuthenticationChoice ::= CHOICE {
    +  /// simple                  [0] OCTET STRING,
    +  /// -- 1 and 2 reserved
    +  /// sasl                    [3] SaslCredentials }
    +  /// 
    + /// + internal class RfcAuthenticationChoice : Asn1Choice { + public RfcAuthenticationChoice(SByte[] passwd) + : base(new Asn1Tagged(new Asn1Identifier(0), new Asn1OctetString(passwd), false)) { + } + + public RfcAuthenticationChoice(String mechanism, SByte[] credentials) + : base(new Asn1Tagged(new Asn1Identifier(3, true), new RfcSaslCredentials(mechanism, credentials), false)) { + // implicit tagging + } + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/Ldap/RfcFilter.cs b/Unosquare.Swan/Networking/Ldap/RfcFilter.cs index 2bb81e2..c7fec13 100644 --- a/Unosquare.Swan/Networking/Ldap/RfcFilter.cs +++ b/Unosquare.Swan/Networking/Ldap/RfcFilter.cs @@ -1,1134 +1,986 @@ -namespace Unosquare.Swan.Networking.Ldap -{ - using System; - using System.Collections; - using System.Collections.Generic; - using System.Text; - using Exceptions; - +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using Unosquare.Swan.Exceptions; + +namespace Unosquare.Swan.Networking.Ldap { + /// + /// Represents an Ldap Filter by parsing an RFC 2254 Search Filter String. + /// This filter object can be created from a String or can be built up + /// programatically by adding filter components one at a time. Existing filter + /// components can be iterated though. + /// Each filter component has an integer identifier defined in this class. + /// The following are basic filter components: {EQUALITY_MATCH}, + /// {GREATER_OR_EQUAL}, {LESS_OR_EQUAL}, {SUBSTRINGS}, + /// {PRESENT}, {APPROX_MATCH}, {EXTENSIBLE_MATCH}. + /// More filters can be nested together into more complex filters with the + /// following filter components: {AND}, {OR}, {NOT} + /// Substrings can have three components:. + /// + ///
    +  /// Filter ::= CHOICE {
    +  /// and             [0] SET OF Filter,
    +  /// or              [1] SET OF Filter,
    +  /// not             [2] Filter,
    +  /// equalityMatch   [3] AttributeValueAssertion,
    +  /// substrings      [4] SubstringFilter,
    +  /// greaterOrEqual  [5] AttributeValueAssertion,
    +  /// lessOrEqual     [6] AttributeValueAssertion,
    +  /// present         [7] AttributeDescription,
    +  /// approxMatch     [8] AttributeValueAssertion,
    +  /// extensibleMatch [9] MatchingRuleAssertion }
    +  /// 
    + ///
    + /// + internal class RfcFilter : Asn1Choice { + private FilterTokenizer _ft; + + private Stack _filterStack; + private Boolean _finalFound; + + public RfcFilter(String filter) => this.ChoiceValue = this.Parse(filter); + /// - /// Represents an Ldap Filter by parsing an RFC 2254 Search Filter String. - /// This filter object can be created from a String or can be built up - /// programatically by adding filter components one at a time. Existing filter - /// components can be iterated though. - /// Each filter component has an integer identifier defined in this class. - /// The following are basic filter components: {EQUALITY_MATCH}, - /// {GREATER_OR_EQUAL}, {LESS_OR_EQUAL}, {SUBSTRINGS}, - /// {PRESENT}, {APPROX_MATCH}, {EXTENSIBLE_MATCH}. - /// More filters can be nested together into more complex filters with the - /// following filter components: {AND}, {OR}, {NOT} - /// Substrings can have three components:. - /// - ///
    -    /// Filter ::= CHOICE {
    -    /// and             [0] SET OF Filter,
    -    /// or              [1] SET OF Filter,
    -    /// not             [2] Filter,
    -    /// equalityMatch   [3] AttributeValueAssertion,
    -    /// substrings      [4] SubstringFilter,
    -    /// greaterOrEqual  [5] AttributeValueAssertion,
    -    /// lessOrEqual     [6] AttributeValueAssertion,
    -    /// present         [7] AttributeDescription,
    -    /// approxMatch     [8] AttributeValueAssertion,
    -    /// extensibleMatch [9] MatchingRuleAssertion }
    -    /// 
    + /// Creates and adds a substrings filter component. + /// startSubstrings must be immediately followed by at least one + /// AddSubstring method and one EndSubstrings method. ///
    - /// - internal class RfcFilter : Asn1Choice - { - private FilterTokenizer _ft; - - private Stack _filterStack; - private bool _finalFound; - - public RfcFilter(string filter) - { - ChoiceValue = Parse(filter); - } - - /// - /// Creates and adds a substrings filter component. - /// startSubstrings must be immediately followed by at least one - /// AddSubstring method and one EndSubstrings method. - /// - /// Name of the attribute. - public void StartSubstrings(string attrName) - { - _finalFound = false; - var seq = new Asn1SequenceOf(5); - - AddObject(new Asn1Tagged(new Asn1Identifier((int)FilterOp.Substrings, true), - new RfcSubstringFilter(attrName, seq), - false)); - _filterStack.Push(seq); - } - - /// - /// Adds a Substring component of initial, any or final substring matching. - /// This method can be invoked only if startSubString was the last filter- - /// building method called. A substring is not required to have an 'INITIAL' - /// substring. However, when a filter contains an 'INITIAL' substring only - /// one can be added, and it must be the first substring added. Any number of - /// 'ANY' substrings can be added. A substring is not required to have a - /// 'FINAL' substrings either. However, when a filter does contain a 'FINAL' - /// substring only one can be added, and it must be the last substring added. - /// - /// Substring type: INITIAL | ANY | FINAL]. - /// The value renamed. - /// - /// Attempt to add an invalid " + "substring type - /// or - /// Attempt to add an initial " + "substring match after the first substring - /// or - /// Attempt to add a substring " + "match after a final substring match - /// or - /// A call to addSubstring occured " + "without calling startSubstring. - /// - public void AddSubstring(SubstringOp type, sbyte[] values) - { - try - { - var substringSeq = (Asn1SequenceOf)_filterStack.Peek(); - if (type != SubstringOp.Initial && type != SubstringOp.Any && type != SubstringOp.Final) - { - throw new LdapException("Attempt to add an invalid substring type", - LdapStatusCode.FilterError); - } - - if (type == SubstringOp.Initial && substringSeq.Size() != 0) - { - throw new LdapException( - "Attempt to add an initial substring match after the first substring", - LdapStatusCode.FilterError); - } - - if (_finalFound) - { - throw new LdapException("Attempt to add a substring match after a final substring match", - LdapStatusCode.FilterError); - } - - if (type == SubstringOp.Final) - { - _finalFound = true; - } - - substringSeq.Add(new Asn1Tagged(new Asn1Identifier((int)type), values)); - } - catch (InvalidCastException) - { - throw new LdapException("A call to addSubstring occured without calling startSubstring", - LdapStatusCode.FilterError); - } - } - - /// - /// Completes a SubString filter component. - /// - /// - /// Empty substring filter - /// or - /// Mismatched ending of substrings. - /// - public void EndSubstrings() - { - try - { - _finalFound = false; - var substringSeq = (Asn1SequenceOf)_filterStack.Peek(); - - if (substringSeq.Size() == 0) - { - throw new LdapException("Empty substring filter", LdapStatusCode.FilterError); - } - } - catch (InvalidCastException) - { - throw new LdapException("Mismatched ending of substrings", LdapStatusCode.FilterError); - } - - _filterStack.Pop(); - } - - /// - /// Creates and adds an AttributeValueAssertion to the filter. - /// - /// Filter type: EQUALITY_MATCH | GREATER_OR_EQUAL - /// | LESS_OR_EQUAL | APPROX_MATCH ]. - /// Name of the attribute to be asserted. - /// Value of the attribute to be asserted. - /// - /// Cannot insert an attribute assertion in a substring - /// or - /// Invalid filter type for AttributeValueAssertion. - /// - public void AddAttributeValueAssertion(FilterOp rfcType, string attrName, sbyte[] valueArray) - { - if (_filterStack != null && _filterStack.Count != 0 && _filterStack.Peek() is Asn1SequenceOf) - { - throw new LdapException("Cannot insert an attribute assertion in a substring", - LdapStatusCode.FilterError); - } - - if (rfcType != FilterOp.EqualityMatch && rfcType != FilterOp.GreaterOrEqual && - rfcType != FilterOp.LessOrEqual && - rfcType != FilterOp.ApproxMatch) - { - throw new LdapException("Invalid filter type for AttributeValueAssertion", - LdapStatusCode.FilterError); - } - - Asn1Object current = new Asn1Tagged( - new Asn1Identifier((int)rfcType, true), - new RfcAttributeValueAssertion(attrName, valueArray), - false); - AddObject(current); - } - - /// - /// Creates and adds a present matching to the filter. - /// - /// Name of the attribute to check for presence. - public void AddPresent(string attrName) - { - Asn1Object current = new Asn1Tagged( - new Asn1Identifier((int)FilterOp.Present), - new Asn1OctetString(attrName), - false); - AddObject(current); - } - - /// - /// Creates and adds the Asn1Tagged value for a nestedFilter: AND, OR, or - /// NOT. - /// Note that a Not nested filter can only have one filter, where AND - /// and OR do not. - /// - /// Filter type: - /// [AND | OR | NOT]. - /// Attempt to create a nested filter other than AND, OR or NOT. - public void StartNestedFilter(FilterOp rfcType) - { - Asn1Object current; - - switch (rfcType) - { - case FilterOp.And: - case FilterOp.Or: - current = new Asn1Tagged(new Asn1Identifier((int)rfcType, true), new Asn1SetOf(), false); - break; - case FilterOp.Not: - current = new Asn1Tagged(new Asn1Identifier((int)rfcType, true)); - break; - default: - throw new LdapException("Attempt to create a nested filter other than AND, OR or NOT", - LdapStatusCode.FilterError); - } - - AddObject(current); - } - - /// - /// Completes a nested filter and checks for the valid filter type. - /// - /// Type of filter to complete. - /// Mismatched ending of nested filter. - public void EndNestedFilter(FilterOp rfcType) - { - if (rfcType == FilterOp.Not) - { - // if this is a Not than Not should be the second thing on the stack - _filterStack.Pop(); - } - - var topOfStackType = _filterStack.Peek().GetIdentifier().Tag; - if (topOfStackType != (int)rfcType) - { - throw new LdapException("Mismatched ending of nested filter", LdapStatusCode.FilterError); - } - - _filterStack.Pop(); - } - - public IEnumerator GetFilterIterator() => new FilterIterator(this, (Asn1Tagged)ChoiceValue); - - /// - /// Creates and returns a String representation of this filter. - /// - /// Filtered string. - public string FilterToString() - { - var filter = new StringBuilder(); - StringFilter(GetFilterIterator(), filter); - return filter.ToString(); - } - - private static void StringFilter(IEnumerator itr, StringBuilder filter) - { - filter.Append('('); - while (itr.MoveNext()) - { - var filterpart = itr.Current; - - if (filterpart is int i) - { - var op = (FilterOp)i; - switch (op) - { - case FilterOp.And: - filter.Append('&'); - break; - case FilterOp.Or: - filter.Append('|'); - break; - case FilterOp.Not: - filter.Append('!'); - break; - case FilterOp.EqualityMatch: - { - filter.Append((string)itr.Current); - filter.Append('='); - filter.Append(Encoding.UTF8.GetString((sbyte[])itr.Current)); - break; - } - - case FilterOp.GreaterOrEqual: - { - filter - .Append((string)itr.Current) - .Append(">=") - .Append(Encoding.UTF8.GetString((sbyte[])itr.Current)); - break; - } - - case FilterOp.LessOrEqual: - { - filter.Append((string)itr.Current); - filter.Append("<="); - filter.Append(Encoding.UTF8.GetString((sbyte[])itr.Current)); - break; - } - - case FilterOp.Present: - filter.Append((string)itr.Current); - filter.Append("=*"); - break; - case FilterOp.ApproxMatch: - filter.Append((string)itr.Current); - filter.Append("~="); - filter.Append(Encoding.UTF8.GetString((sbyte[])itr.Current)); - break; - case FilterOp.ExtensibleMatch: - var oid = (string)itr.Current; - filter.Append((string)itr.Current); - filter.Append(':'); - filter.Append(oid); - filter.Append(":="); - filter.Append((string)itr.Current); - break; - case FilterOp.Substrings: - { - filter.Append((string)itr.Current); - filter.Append('='); - - while (itr.MoveNext()) - { - switch ((SubstringOp)(int)itr.Current) - { - case SubstringOp.Any: - case SubstringOp.Initial: - filter.Append(itr.Current as string); - filter.Append('*'); - break; - case SubstringOp.Final: - filter.Append((string)itr.Current); - break; - } - } - - break; - } - } - } - else if (filterpart is IEnumerator enumerator) - { - StringFilter(enumerator, filter); - } - } - - filter.Append(')'); - } - - private Asn1Tagged Parse(string expression) - { - var filterExpr = string.IsNullOrWhiteSpace(expression) ? "(objectclass=*)" : expression; - - int idx; - if ((idx = filterExpr.IndexOf('\\')) != -1) - { - var sb = new StringBuilder(filterExpr); - var i = idx; - while (i < sb.Length - 1) - { - var c = sb[i++]; - - if (c != '\\') continue; - - // found '\' (backslash) - // If V2 escape, turn to a V3 escape - c = sb[i]; - if (c != '*' && c != '(' && c != ')' && c != '\\') continue; - - // Ldap v2 filter, convert them into hex chars - sb.Remove(i, i + 1 - i); - sb.Insert(i, Convert.ToString(c, 16)); - i += 2; - } - - filterExpr = sb.ToString(); - } - - // missing opening and closing parentheses, must be V2, add parentheses - if (filterExpr[0] != '(' && filterExpr[filterExpr.Length - 1] != ')') - { - filterExpr = "(" + filterExpr + ")"; - } - - var ch = filterExpr[0]; - var len = filterExpr.Length; - - // missing opening parenthesis ? - if (ch != '(') - { - throw new LdapException(LdapException.MissingLeftParen, LdapStatusCode.FilterError); - } - - // missing closing parenthesis ? - if (filterExpr[len - 1] != ')') - { - throw new LdapException(LdapException.MissingRightParen, LdapStatusCode.FilterError); - } - - // unmatched parentheses ? - var parenCount = 0; - for (var i = 0; i < len; i++) - { - switch (filterExpr[i]) - { - case '(': - parenCount++; - break; - case ')': - parenCount--; - break; - } - } - - if (parenCount > 0) - { - throw new LdapException(LdapException.MissingRightParen, LdapStatusCode.FilterError); - } - - if (parenCount < 0) - { - throw new LdapException(LdapException.MissingLeftParen, LdapStatusCode.FilterError); - } - - _ft = new FilterTokenizer(this, filterExpr); - return ParseFilter(); - } - - private Asn1Tagged ParseFilter() - { - _ft.GetLeftParen(); - var filter = ParseFilterComp(); - _ft.GetRightParen(); - return filter; - } - - private Asn1Tagged ParseFilterComp() - { - Asn1Tagged tag = null; - var filterComp = (FilterOp)_ft.OpOrAttr; - - switch (filterComp) - { - case FilterOp.And: - case FilterOp.Or: - tag = new Asn1Tagged(new Asn1Identifier((int)filterComp, true), - ParseFilterList(), - false); - break; - case FilterOp.Not: - tag = new Asn1Tagged(new Asn1Identifier((int)filterComp, true), - ParseFilter()); - break; - default: - var filterType = _ft.FilterType; - var valueRenamed = _ft.Value; - - switch (filterType) - { - case FilterOp.GreaterOrEqual: - case FilterOp.LessOrEqual: - case FilterOp.ApproxMatch: - tag = new Asn1Tagged(new Asn1Identifier((int)filterType, true), - new RfcAttributeValueAssertion(_ft.Attr, UnescapeString(valueRenamed)), - false); - break; - case FilterOp.EqualityMatch: - if (valueRenamed.Equals("*")) - { - // present - tag = new Asn1Tagged( - new Asn1Identifier((int)FilterOp.Present), - new Asn1OctetString(_ft.Attr), - false); - } - else if (valueRenamed.IndexOf('*') != -1) - { - var sub = new Tokenizer(valueRenamed, "*", true); - var seq = new Asn1SequenceOf(5); - var tokCnt = sub.Count; - var cnt = 0; - var lastTok = new StringBuilder(string.Empty).ToString(); - while (sub.HasMoreTokens()) - { - var subTok = sub.NextToken(); - cnt++; - if (subTok.Equals("*")) - { - // if previous token was '*', and since the current - // token is a '*', we need to insert 'any' - if (lastTok.Equals(subTok)) - { - // '**' - seq.Add(new Asn1Tagged( - new Asn1Identifier((int)SubstringOp.Any), - UnescapeString(string.Empty))); - } - } - else - { - // value (RfcLdapString) - if (cnt == 1) - { - // initial - seq.Add(new Asn1Tagged( - new Asn1Identifier((int)SubstringOp.Initial), - UnescapeString(subTok))); - } - else if (cnt < tokCnt) - { - // any - seq.Add(new Asn1Tagged( - new Asn1Identifier((int)SubstringOp.Any), - UnescapeString(subTok))); - } - else - { - // final - seq.Add(new Asn1Tagged( - new Asn1Identifier((int)SubstringOp.Final), - UnescapeString(subTok))); - } - } - - lastTok = subTok; - } - - tag = new Asn1Tagged( - new Asn1Identifier((int)FilterOp.Substrings, true), - new RfcSubstringFilter(_ft.Attr, seq), - false); - } - else - { - tag = new Asn1Tagged( - new Asn1Identifier((int)FilterOp.EqualityMatch, true), - new RfcAttributeValueAssertion(_ft.Attr, UnescapeString(valueRenamed)), - false); - } - - break; - case FilterOp.ExtensibleMatch: - string type = null, matchingRule = null; - var attr = false; - var st = new Tokenizer(_ft.Attr, ":"); - var first = true; - - while (st.HasMoreTokens()) - { - var s = st.NextToken().Trim(); - if (first && !s.Equals(":")) - { - type = s; - } - else if (s.Equals("dn")) - { - attr = true; - } - else if (!s.Equals(":")) - { - matchingRule = s; - } - - first = false; - } - - tag = new Asn1Tagged( - new Asn1Identifier((int)FilterOp.ExtensibleMatch, true), - new RfcMatchingRuleAssertion(matchingRule, type, UnescapeString(valueRenamed), attr == false ? null : new Asn1Boolean(true)), - false); - - break; - } - - break; - } - - return tag; - } - - private Asn1SetOf ParseFilterList() - { - var setOf = new Asn1SetOf(); - setOf.Add(ParseFilter()); // must have at least 1 filter - while (_ft.PeekChar() == '(') - { - // check for more filters - setOf.Add(ParseFilter()); - } - - return setOf; - } - - /// - /// Replace escaped hex digits with the equivalent binary representation. - /// Assume either V2 or V3 escape mechanisms: - /// V2: \*, \(, \), \\. - /// V3: \2A, \28, \29, \5C, \00. - /// - /// The string renamed. - /// - /// octet-string encoding of the specified string. - /// - /// Invalid Escape. - private static sbyte[] UnescapeString(string value) - { - var octets = new sbyte[value.Length * 3]; - var octs = 0; - var escape = false; - var escStart = false; - var length = value.Length; - var ca = new char[1]; // used while converting multibyte UTF-8 char - var temp = (char)0; // holds the value of the escaped sequence - - // loop through each character of the string and copy them into octets - // converting escaped sequences when needed - for (var str = 0; str < length; str++) - { - var ch = value[str]; // Character we are adding to the octet string - - if (escape) - { - var ival = ch.Hex2Int(); - - if (ival < 0) - { - throw new LdapException($"Invalid value in escape sequence \"{ch}\"", - LdapStatusCode.FilterError); - } - - // V3 escaped: \\** - if (escStart) - { - temp = (char)(ival << 4); // high bits of escaped char - escStart = false; - } - else - { - temp |= (char)ival; // all bits of escaped char - octets[octs++] = (sbyte)temp; - } - } - else if (ch == '\\') - { - escStart = escape = true; - } - else - { - // place the character into octets. - if ((ch >= 0x01 && ch <= 0x27) || (ch >= 0x2B && ch <= 0x5B) || ch >= 0x5D) - { - // found valid char - if (ch <= 0x7f) - { - // char = %x01-27 / %x2b-5b / %x5d-7f - octets[octs++] = (sbyte)ch; - } - else - { - // char > 0x7f, could be encoded in 2 or 3 bytes - ca[0] = ch; - var utf8Bytes = Encoding.UTF8.GetSBytes(new string(ca)); - - // copy utf8 encoded character into octets - Array.Copy(utf8Bytes, 0, octets, octs, utf8Bytes.Length); - octs = octs + utf8Bytes.Length; - } - } - else - { - // found invalid character - var escString = new StringBuilder(); - ca[0] = ch; - - foreach (var u in Encoding.UTF8.GetSBytes(new string(ca))) - { - escString.Append("\\"); - - if (u >= 0 && u < 0x10) escString.Append("0"); - - escString.Append(Convert.ToString(u & 0xff, 16)); - } - - throw new LdapException( - $"The invalid character \"{ch}\" needs to be escaped as \"{escString}\"", - LdapStatusCode.FilterError); - } - } - } - - // Verify that any escape sequence completed - if (escStart || escape) - { - throw new LdapException("Incomplete escape sequence", LdapStatusCode.FilterError); - } - - var toReturn = new sbyte[octs]; - Array.Copy(octets, 0, toReturn, 0, octs); - - return toReturn; - } - - private void AddObject(Asn1Object current) - { - if (_filterStack == null) - { - _filterStack = new Stack(); - } - - if (ChoiceValue == null) - { - // ChoiceValue is the root Asn1 node - ChoiceValue = current; - } - else - { - var topOfStack = (Asn1Tagged)_filterStack.Peek(); - var value = topOfStack.TaggedValue; - - switch (value) - { - case null: - topOfStack.TaggedValue = current; - _filterStack.Push(current); - break; - case Asn1SetOf _: - ((Asn1SetOf)value).Add(current); - break; - case Asn1Set _: - ((Asn1Set)value).Add(current); - break; - default: - if (value.GetIdentifier().Tag == (int)FilterOp.Not) - { - throw new LdapException("Attempt to create more than one 'not' sub-filter", - LdapStatusCode.FilterError); - } - - break; - } - } - - var type = (FilterOp)current.GetIdentifier().Tag; - if (type == FilterOp.And || type == FilterOp.Or || type == FilterOp.Not) - { - _filterStack.Push(current); - } - } - - internal class FilterTokenizer - { - private readonly string _filter; // The filter string to parse - private readonly int _filterLength; // Length of the filter string to parse - private int _offset; // Offset pointer into the filter string - - public FilterTokenizer(RfcFilter enclosingInstance, string filter) - { - EnclosingInstance = enclosingInstance; - _filter = filter; - _offset = 0; - _filterLength = filter.Length; - } - - /// - /// Reads either an operator, or an attribute, whichever is - /// next in the filter string. - /// If the next component is an attribute, it is read and stored in the - /// attr field of this class which may be retrieved with getAttr() - /// and a -1 is returned. Otherwise, the int value of the operator read is - /// returned. - /// - /// - /// The op or attribute. - /// - /// Unexpected end. - public int OpOrAttr - { - get - { - if (_offset >= _filterLength) - { - throw new LdapException(LdapException.UnexpectedEnd, LdapStatusCode.FilterError); - } - - int ret; - int testChar = _filter[_offset]; - - switch (testChar) - { - case '&': - _offset++; - ret = (int)FilterOp.And; - break; - case '|': - _offset++; - ret = (int)FilterOp.Or; - break; - case '!': - _offset++; - ret = (int)FilterOp.Not; - break; - default: - if (_filter.Substring(_offset).StartsWith(":=")) - { - throw new LdapException("Missing matching rule", LdapStatusCode.FilterError); - } - - if (_filter.Substring(_offset).StartsWith("::=") || - _filter.Substring(_offset).StartsWith(":::=")) - { - throw new LdapException("DN and matching rule not specified", LdapStatusCode.FilterError); - } - - // get first component of 'item' (attr or :dn or :matchingrule) - const string delims = "=~<>()"; - var sb = new StringBuilder(); - while (delims.IndexOf(_filter[_offset]) == -1 && - _filter.Substring(_offset).StartsWith(":=") == false) - { - sb.Append(_filter[_offset++]); - } - - Attr = sb.ToString().Trim(); - - // is there an attribute name specified in the filter ? - if (Attr.Length == 0 || Attr[0] == ';') - { - throw new LdapException("Missing attribute description", LdapStatusCode.FilterError); - } - - int index; - for (index = 0; index < Attr.Length; index++) - { - var atIndex = Attr[index]; - if (char.IsLetterOrDigit(atIndex) || atIndex == '-' || atIndex == '.' || - atIndex == ';' || - atIndex == ':') continue; - - if (atIndex == '\\') - { - throw new LdapException("Escape sequence not allowed in attribute description", - LdapStatusCode.FilterError); - } - - throw new LdapException($"Invalid character \"{atIndex}\" in attribute description", - LdapStatusCode.FilterError); - } - - // is there an option specified in the filter ? - index = Attr.IndexOf(';'); - if (index != -1 && index == Attr.Length - 1) - { - throw new LdapException("Semicolon present, but no option specified", - LdapStatusCode.FilterError); - } - - ret = -1; - break; - } - - return ret; - } - } - - public FilterOp FilterType - { - get - { - if (_offset >= _filterLength) - { - throw new LdapException(LdapException.UnexpectedEnd, LdapStatusCode.FilterError); - } - - if (_filter.Substring(_offset).StartsWith(">=")) - { - _offset += 2; - return FilterOp.GreaterOrEqual; - } - - if (_filter.Substring(_offset).StartsWith("<=")) - { - _offset += 2; - return FilterOp.LessOrEqual; - } - - if (_filter.Substring(_offset).StartsWith("~=")) - { - _offset += 2; - return FilterOp.ApproxMatch; - } - - if (_filter.Substring(_offset).StartsWith(":=")) - { - _offset += 2; - return FilterOp.ExtensibleMatch; - } - - if (_filter[_offset] == '=') - { - _offset++; - return FilterOp.EqualityMatch; - } - - throw new LdapException("Invalid comparison operator", LdapStatusCode.FilterError); - } - } - - public string Value - { - get - { - if (_offset >= _filterLength) - { - throw new LdapException(LdapException.UnexpectedEnd, LdapStatusCode.FilterError); - } - - var idx = _filter.IndexOf(')', _offset); - if (idx == -1) - { - idx = _filterLength; - } - - var ret = _filter.Substring(_offset, idx - _offset); - _offset = idx; - return ret; - } - } - - public string Attr { get; private set; } - - public RfcFilter EnclosingInstance { get; } - - public void GetLeftParen() - { - if (_offset >= _filterLength) - { - throw new LdapException(LdapException.UnexpectedEnd, LdapStatusCode.FilterError); - } - - if (_filter[_offset++] != '(') - { - throw new LdapException(string.Format(LdapException.ExpectingLeftParen, _filter[_offset -= 1]), - LdapStatusCode.FilterError); - } - } - - public void GetRightParen() - { - if (_offset >= _filterLength) - { - throw new LdapException(LdapException.UnexpectedEnd, LdapStatusCode.FilterError); - } - - if (_filter[_offset++] != ')') - { - throw new LdapException(string.Format(LdapException.ExpectingRightParen, _filter[_offset - 1]), - LdapStatusCode.FilterError); - } - } - - public char PeekChar() - { - if (_offset >= _filterLength) - { - throw new LdapException(LdapException.UnexpectedEnd, LdapStatusCode.FilterError); - } - - return _filter[_offset]; - } - } - - /// - /// This inner class wrappers the Search Filter with an iterator. - /// This iterator will give access to all the individual components - /// pre-parsed. The first call to next will return an Integer identifying - /// the type of filter component. Then the component values will be returned - /// AND, NOT, and OR components values will be returned as Iterators. - /// - /// - private sealed class FilterIterator - : IEnumerator - { - private readonly Asn1Tagged _root; - - private readonly RfcFilter _enclosingInstance; - - private bool _tagReturned; - - private int _index = -1; - - private bool _hasMore = true; - - public FilterIterator(RfcFilter enclosingInstance, Asn1Tagged root) - { - _enclosingInstance = enclosingInstance; - _root = root; - } - - public object Current - { - get - { - object toReturn = null; - - if (!_tagReturned) - { - _tagReturned = true; - toReturn = _root.GetIdentifier().Tag; - } - else - { - var asn1 = _root.TaggedValue; - - switch (asn1) - { - case Asn1OctetString s: - - // one value to iterate - _hasMore = false; - toReturn = s.StringValue(); - break; - case RfcSubstringFilter sub: - if (_index == -1) - { - // return attribute name - _index = 0; - var attr = (Asn1OctetString)sub.Get(0); - toReturn = attr.StringValue(); - } - else if (_index % 2 == 0) - { - // return substring identifier - var substrs = (Asn1SequenceOf)sub.Get(1); - toReturn = ((Asn1Tagged)substrs.Get(_index / 2)).GetIdentifier().Tag; - _index++; - } - else - { - // return substring value - var substrs = (Asn1SequenceOf)sub.Get(1); - var tag = (Asn1Tagged)substrs.Get(_index / 2); - toReturn = ((Asn1OctetString)tag.TaggedValue).StringValue(); - _index++; - } - - if (_index / 2 >= ((Asn1SequenceOf)sub.Get(1)).Size()) - { - _hasMore = false; - } - - break; - case RfcAttributeValueAssertion assertion: - - // components: =,>=,<=,~= - if (_index == -1) - { - toReturn = assertion.AttributeDescription; - _index = 1; - } - else if (_index == 1) - { - toReturn = assertion.AssertionValue; - _index = 2; - _hasMore = false; - } - - break; - case RfcMatchingRuleAssertion exMatch: - - // Extensible match - if (_index == -1) - { - _index = 0; - } - - toReturn = - ((Asn1OctetString)((Asn1Tagged)exMatch.Get(_index++)).TaggedValue) - .StringValue(); - if (_index > 2) - { - _hasMore = false; - } - - break; - case Asn1SetOf setRenamed: - - // AND and OR nested components - if (_index == -1) - { - _index = 0; - } - - toReturn = new FilterIterator(_enclosingInstance, - (Asn1Tagged)setRenamed.Get(_index++)); - if (_index >= setRenamed.Size()) - { - _hasMore = false; - } - - break; - case Asn1Tagged _: - - // NOT nested component. - toReturn = new FilterIterator(_enclosingInstance, (Asn1Tagged)asn1); - _hasMore = false; - break; - } - } - - return toReturn; - } - } - - public void Reset() - { - // do nothing - } - - public bool MoveNext() => _hasMore; - } - } + /// Name of the attribute. + public void StartSubstrings(String attrName) { + this._finalFound = false; + Asn1SequenceOf seq = new Asn1SequenceOf(5); + + this.AddObject(new Asn1Tagged(new Asn1Identifier((Int32)FilterOp.Substrings, true), + new RfcSubstringFilter(attrName, seq), + false)); + this._filterStack.Push(seq); + } + + /// + /// Adds a Substring component of initial, any or final substring matching. + /// This method can be invoked only if startSubString was the last filter- + /// building method called. A substring is not required to have an 'INITIAL' + /// substring. However, when a filter contains an 'INITIAL' substring only + /// one can be added, and it must be the first substring added. Any number of + /// 'ANY' substrings can be added. A substring is not required to have a + /// 'FINAL' substrings either. However, when a filter does contain a 'FINAL' + /// substring only one can be added, and it must be the last substring added. + /// + /// Substring type: INITIAL | ANY | FINAL]. + /// The value renamed. + /// + /// Attempt to add an invalid " + "substring type + /// or + /// Attempt to add an initial " + "substring match after the first substring + /// or + /// Attempt to add a substring " + "match after a final substring match + /// or + /// A call to addSubstring occured " + "without calling startSubstring. + /// + public void AddSubstring(SubstringOp type, SByte[] values) { + try { + Asn1SequenceOf substringSeq = (Asn1SequenceOf)this._filterStack.Peek(); + if(type != SubstringOp.Initial && type != SubstringOp.Any && type != SubstringOp.Final) { + throw new LdapException("Attempt to add an invalid substring type", + LdapStatusCode.FilterError); + } + + if(type == SubstringOp.Initial && substringSeq.Size() != 0) { + throw new LdapException( + "Attempt to add an initial substring match after the first substring", + LdapStatusCode.FilterError); + } + + if(this._finalFound) { + throw new LdapException("Attempt to add a substring match after a final substring match", + LdapStatusCode.FilterError); + } + + if(type == SubstringOp.Final) { + this._finalFound = true; + } + + substringSeq.Add(new Asn1Tagged(new Asn1Identifier((Int32)type), values)); + } catch(InvalidCastException) { + throw new LdapException("A call to addSubstring occured without calling startSubstring", + LdapStatusCode.FilterError); + } + } + + /// + /// Completes a SubString filter component. + /// + /// + /// Empty substring filter + /// or + /// Mismatched ending of substrings. + /// + public void EndSubstrings() { + try { + this._finalFound = false; + Asn1SequenceOf substringSeq = (Asn1SequenceOf)this._filterStack.Peek(); + + if(substringSeq.Size() == 0) { + throw new LdapException("Empty substring filter", LdapStatusCode.FilterError); + } + } catch(InvalidCastException) { + throw new LdapException("Mismatched ending of substrings", LdapStatusCode.FilterError); + } + + _ = this._filterStack.Pop(); + } + + /// + /// Creates and adds an AttributeValueAssertion to the filter. + /// + /// Filter type: EQUALITY_MATCH | GREATER_OR_EQUAL + /// | LESS_OR_EQUAL | APPROX_MATCH ]. + /// Name of the attribute to be asserted. + /// Value of the attribute to be asserted. + /// + /// Cannot insert an attribute assertion in a substring + /// or + /// Invalid filter type for AttributeValueAssertion. + /// + public void AddAttributeValueAssertion(FilterOp rfcType, String attrName, SByte[] valueArray) { + if(this._filterStack != null && this._filterStack.Count != 0 && this._filterStack.Peek() is Asn1SequenceOf) { + throw new LdapException("Cannot insert an attribute assertion in a substring", + LdapStatusCode.FilterError); + } + + if(rfcType != FilterOp.EqualityMatch && rfcType != FilterOp.GreaterOrEqual && + rfcType != FilterOp.LessOrEqual && + rfcType != FilterOp.ApproxMatch) { + throw new LdapException("Invalid filter type for AttributeValueAssertion", + LdapStatusCode.FilterError); + } + + Asn1Object current = new Asn1Tagged( + new Asn1Identifier((Int32)rfcType, true), + new RfcAttributeValueAssertion(attrName, valueArray), + false); + this.AddObject(current); + } + + /// + /// Creates and adds a present matching to the filter. + /// + /// Name of the attribute to check for presence. + public void AddPresent(String attrName) { + Asn1Object current = new Asn1Tagged( + new Asn1Identifier((Int32)FilterOp.Present), + new Asn1OctetString(attrName), + false); + this.AddObject(current); + } + + /// + /// Creates and adds the Asn1Tagged value for a nestedFilter: AND, OR, or + /// NOT. + /// Note that a Not nested filter can only have one filter, where AND + /// and OR do not. + /// + /// Filter type: + /// [AND | OR | NOT]. + /// Attempt to create a nested filter other than AND, OR or NOT. + public void StartNestedFilter(FilterOp rfcType) { + Asn1Object current; + + switch(rfcType) { + case FilterOp.And: + case FilterOp.Or: + current = new Asn1Tagged(new Asn1Identifier((Int32)rfcType, true), new Asn1SetOf(), false); + break; + case FilterOp.Not: + current = new Asn1Tagged(new Asn1Identifier((Int32)rfcType, true)); + break; + default: + throw new LdapException("Attempt to create a nested filter other than AND, OR or NOT", + LdapStatusCode.FilterError); + } + + this.AddObject(current); + } + + /// + /// Completes a nested filter and checks for the valid filter type. + /// + /// Type of filter to complete. + /// Mismatched ending of nested filter. + public void EndNestedFilter(FilterOp rfcType) { + if(rfcType == FilterOp.Not) { + // if this is a Not than Not should be the second thing on the stack + _ = this._filterStack.Pop(); + } + + Int32 topOfStackType = this._filterStack.Peek().GetIdentifier().Tag; + if(topOfStackType != (Int32)rfcType) { + throw new LdapException("Mismatched ending of nested filter", LdapStatusCode.FilterError); + } + + _ = this._filterStack.Pop(); + } + + public IEnumerator GetFilterIterator() => new FilterIterator(this, (Asn1Tagged)this.ChoiceValue); + + /// + /// Creates and returns a String representation of this filter. + /// + /// Filtered string. + public String FilterToString() { + StringBuilder filter = new StringBuilder(); + StringFilter(this.GetFilterIterator(), filter); + return filter.ToString(); + } + + private static void StringFilter(IEnumerator itr, StringBuilder filter) { + _ = filter.Append('('); + while(itr.MoveNext()) { + Object filterpart = itr.Current; + + if(filterpart is Int32 i) { + FilterOp op = (FilterOp)i; + switch(op) { + case FilterOp.And: + _ = filter.Append('&'); + break; + case FilterOp.Or: + _ = filter.Append('|'); + break; + case FilterOp.Not: + _ = filter.Append('!'); + break; + case FilterOp.EqualityMatch: { + _ = filter.Append((String)itr.Current); + _ = filter.Append('='); + _ = filter.Append(Encoding.UTF8.GetString((SByte[])itr.Current)); + break; + } + + case FilterOp.GreaterOrEqual: { + _ = filter + .Append((String)itr.Current) + .Append(">=") + .Append(Encoding.UTF8.GetString((SByte[])itr.Current)); + break; + } + + case FilterOp.LessOrEqual: { + _ = filter.Append((String)itr.Current); + _ = filter.Append("<="); + _ = filter.Append(Encoding.UTF8.GetString((SByte[])itr.Current)); + break; + } + + case FilterOp.Present: + _ = filter.Append((String)itr.Current); + _ = filter.Append("=*"); + break; + case FilterOp.ApproxMatch: + _ = filter.Append((String)itr.Current); + _ = filter.Append("~="); + _ = filter.Append(Encoding.UTF8.GetString((SByte[])itr.Current)); + break; + case FilterOp.ExtensibleMatch: + String oid = (String)itr.Current; + _ = filter.Append((String)itr.Current); + _ = filter.Append(':'); + _ = filter.Append(oid); + _ = filter.Append(":="); + _ = filter.Append((String)itr.Current); + break; + case FilterOp.Substrings: { + _ = filter.Append((String)itr.Current); + _ = filter.Append('='); + + while(itr.MoveNext()) { + switch((SubstringOp)(Int32)itr.Current) { + case SubstringOp.Any: + case SubstringOp.Initial: + _ = filter.Append(itr.Current as String); + _ = filter.Append('*'); + break; + case SubstringOp.Final: + _ = filter.Append((String)itr.Current); + break; + } + } + + break; + } + } + } else if(filterpart is IEnumerator enumerator) { + StringFilter(enumerator, filter); + } + } + + _ = filter.Append(')'); + } + + private Asn1Tagged Parse(String expression) { + String filterExpr = String.IsNullOrWhiteSpace(expression) ? "(objectclass=*)" : expression; + + Int32 idx; + if((idx = filterExpr.IndexOf('\\')) != -1) { + StringBuilder sb = new StringBuilder(filterExpr); + Int32 i = idx; + while(i < sb.Length - 1) { + Char c = sb[i++]; + + if(c != '\\') { + continue; + } + + // found '\' (backslash) + // If V2 escape, turn to a V3 escape + c = sb[i]; + if(c != '*' && c != '(' && c != ')' && c != '\\') { + continue; + } + + // Ldap v2 filter, convert them into hex chars + _ = sb.Remove(i, i + 1 - i); + _ = sb.Insert(i, Convert.ToString(c, 16)); + i += 2; + } + + filterExpr = sb.ToString(); + } + + // missing opening and closing parentheses, must be V2, add parentheses + if(filterExpr[0] != '(' && filterExpr[filterExpr.Length - 1] != ')') { + filterExpr = "(" + filterExpr + ")"; + } + + Char ch = filterExpr[0]; + Int32 len = filterExpr.Length; + + // missing opening parenthesis ? + if(ch != '(') { + throw new LdapException(LdapException.MissingLeftParen, LdapStatusCode.FilterError); + } + + // missing closing parenthesis ? + if(filterExpr[len - 1] != ')') { + throw new LdapException(LdapException.MissingRightParen, LdapStatusCode.FilterError); + } + + // unmatched parentheses ? + Int32 parenCount = 0; + for(Int32 i = 0; i < len; i++) { + switch(filterExpr[i]) { + case '(': + parenCount++; + break; + case ')': + parenCount--; + break; + } + } + + if(parenCount > 0) { + throw new LdapException(LdapException.MissingRightParen, LdapStatusCode.FilterError); + } + + if(parenCount < 0) { + throw new LdapException(LdapException.MissingLeftParen, LdapStatusCode.FilterError); + } + + this._ft = new FilterTokenizer(this, filterExpr); + return this.ParseFilter(); + } + + private Asn1Tagged ParseFilter() { + this._ft.GetLeftParen(); + Asn1Tagged filter = this.ParseFilterComp(); + this._ft.GetRightParen(); + return filter; + } + + private Asn1Tagged ParseFilterComp() { + Asn1Tagged tag = null; + FilterOp filterComp = (FilterOp)this._ft.OpOrAttr; + + switch(filterComp) { + case FilterOp.And: + case FilterOp.Or: + tag = new Asn1Tagged(new Asn1Identifier((Int32)filterComp, true), + this.ParseFilterList(), + false); + break; + case FilterOp.Not: + tag = new Asn1Tagged(new Asn1Identifier((Int32)filterComp, true), + this.ParseFilter()); + break; + default: + FilterOp filterType = this._ft.FilterType; + String valueRenamed = this._ft.Value; + + switch(filterType) { + case FilterOp.GreaterOrEqual: + case FilterOp.LessOrEqual: + case FilterOp.ApproxMatch: + tag = new Asn1Tagged(new Asn1Identifier((Int32)filterType, true), + new RfcAttributeValueAssertion(this._ft.Attr, UnescapeString(valueRenamed)), + false); + break; + case FilterOp.EqualityMatch: + if(valueRenamed.Equals("*")) { + // present + tag = new Asn1Tagged( + new Asn1Identifier((Int32)FilterOp.Present), + new Asn1OctetString(this._ft.Attr), + false); + } else if(valueRenamed.IndexOf('*') != -1) { + Tokenizer sub = new Tokenizer(valueRenamed, "*", true); + Asn1SequenceOf seq = new Asn1SequenceOf(5); + Int32 tokCnt = sub.Count; + Int32 cnt = 0; + String lastTok = new StringBuilder(String.Empty).ToString(); + while(sub.HasMoreTokens()) { + String subTok = sub.NextToken(); + cnt++; + if(subTok.Equals("*")) { + // if previous token was '*', and since the current + // token is a '*', we need to insert 'any' + if(lastTok.Equals(subTok)) { + // '**' + seq.Add(new Asn1Tagged( + new Asn1Identifier((Int32)SubstringOp.Any), + UnescapeString(String.Empty))); + } + } else { + // value (RfcLdapString) + if(cnt == 1) { + // initial + seq.Add(new Asn1Tagged( + new Asn1Identifier((Int32)SubstringOp.Initial), + UnescapeString(subTok))); + } else if(cnt < tokCnt) { + // any + seq.Add(new Asn1Tagged( + new Asn1Identifier((Int32)SubstringOp.Any), + UnescapeString(subTok))); + } else { + // final + seq.Add(new Asn1Tagged( + new Asn1Identifier((Int32)SubstringOp.Final), + UnescapeString(subTok))); + } + } + + lastTok = subTok; + } + + tag = new Asn1Tagged( + new Asn1Identifier((Int32)FilterOp.Substrings, true), + new RfcSubstringFilter(this._ft.Attr, seq), + false); + } else { + tag = new Asn1Tagged( + new Asn1Identifier((Int32)FilterOp.EqualityMatch, true), + new RfcAttributeValueAssertion(this._ft.Attr, UnescapeString(valueRenamed)), + false); + } + + break; + case FilterOp.ExtensibleMatch: + String type = null, matchingRule = null; + Boolean attr = false; + Tokenizer st = new Tokenizer(this._ft.Attr, ":"); + Boolean first = true; + + while(st.HasMoreTokens()) { + String s = st.NextToken().Trim(); + if(first && !s.Equals(":")) { + type = s; + } else if(s.Equals("dn")) { + attr = true; + } else if(!s.Equals(":")) { + matchingRule = s; + } + + first = false; + } + + tag = new Asn1Tagged( + new Asn1Identifier((Int32)FilterOp.ExtensibleMatch, true), + new RfcMatchingRuleAssertion(matchingRule, type, UnescapeString(valueRenamed), attr == false ? null : new Asn1Boolean(true)), + false); + + break; + } + + break; + } + + return tag; + } + + private Asn1SetOf ParseFilterList() { + Asn1SetOf setOf = new Asn1SetOf(); + setOf.Add(this.ParseFilter()); // must have at least 1 filter + while(this._ft.PeekChar() == '(') { + // check for more filters + setOf.Add(this.ParseFilter()); + } + + return setOf; + } + + /// + /// Replace escaped hex digits with the equivalent binary representation. + /// Assume either V2 or V3 escape mechanisms: + /// V2: \*, \(, \), \\. + /// V3: \2A, \28, \29, \5C, \00. + /// + /// The string renamed. + /// + /// octet-string encoding of the specified string. + /// + /// Invalid Escape. + private static SByte[] UnescapeString(String value) { + SByte[] octets = new SByte[value.Length * 3]; + Int32 octs = 0; + Boolean escape = false; + Boolean escStart = false; + Int32 length = value.Length; + Char[] ca = new Char[1]; // used while converting multibyte UTF-8 char + Char temp = (Char)0; // holds the value of the escaped sequence + + // loop through each character of the string and copy them into octets + // converting escaped sequences when needed + for(Int32 str = 0; str < length; str++) { + Char ch = value[str]; // Character we are adding to the octet string + + if(escape) { + Int32 ival = ch.Hex2Int(); + + if(ival < 0) { + throw new LdapException($"Invalid value in escape sequence \"{ch}\"", + LdapStatusCode.FilterError); + } + + // V3 escaped: \\** + if(escStart) { + temp = (Char)(ival << 4); // high bits of escaped char + escStart = false; + } else { + temp |= (Char)ival; // all bits of escaped char + octets[octs++] = (SByte)temp; + } + } else if(ch == '\\') { + escStart = escape = true; + } else { + // place the character into octets. + if(ch >= 0x01 && ch <= 0x27 || ch >= 0x2B && ch <= 0x5B || ch >= 0x5D) { + // found valid char + if(ch <= 0x7f) { + // char = %x01-27 / %x2b-5b / %x5d-7f + octets[octs++] = (SByte)ch; + } else { + // char > 0x7f, could be encoded in 2 or 3 bytes + ca[0] = ch; + SByte[] utf8Bytes = Encoding.UTF8.GetSBytes(new String(ca)); + + // copy utf8 encoded character into octets + Array.Copy(utf8Bytes, 0, octets, octs, utf8Bytes.Length); + octs += utf8Bytes.Length; + } + } else { + // found invalid character + StringBuilder escString = new StringBuilder(); + ca[0] = ch; + + foreach(SByte u in Encoding.UTF8.GetSBytes(new String(ca))) { + _ = escString.Append("\\"); + + if(u >= 0 && u < 0x10) { + _ = escString.Append("0"); + } + + _ = escString.Append(Convert.ToString(u & 0xff, 16)); + } + + throw new LdapException( + $"The invalid character \"{ch}\" needs to be escaped as \"{escString}\"", + LdapStatusCode.FilterError); + } + } + } + + // Verify that any escape sequence completed + if(escStart || escape) { + throw new LdapException("Incomplete escape sequence", LdapStatusCode.FilterError); + } + + SByte[] toReturn = new SByte[octs]; + Array.Copy(octets, 0, toReturn, 0, octs); + + return toReturn; + } + + private void AddObject(Asn1Object current) { + if(this._filterStack == null) { + this._filterStack = new Stack(); + } + + if(this.ChoiceValue == null) { + // ChoiceValue is the root Asn1 node + this.ChoiceValue = current; + } else { + Asn1Tagged topOfStack = (Asn1Tagged)this._filterStack.Peek(); + Asn1Object value = topOfStack.TaggedValue; + + switch(value) { + case null: + topOfStack.TaggedValue = current; + this._filterStack.Push(current); + break; + case Asn1SetOf _: + ((Asn1SetOf)value).Add(current); + break; + case Asn1Set _: + ((Asn1Set)value).Add(current); + break; + default: + if(value.GetIdentifier().Tag == (Int32)FilterOp.Not) { + throw new LdapException("Attempt to create more than one 'not' sub-filter", + LdapStatusCode.FilterError); + } + + break; + } + } + + FilterOp type = (FilterOp)current.GetIdentifier().Tag; + if(type == FilterOp.And || type == FilterOp.Or || type == FilterOp.Not) { + this._filterStack.Push(current); + } + } + + internal class FilterTokenizer { + private readonly String _filter; // The filter string to parse + private readonly Int32 _filterLength; // Length of the filter string to parse + private Int32 _offset; // Offset pointer into the filter string + + public FilterTokenizer(RfcFilter enclosingInstance, String filter) { + this.EnclosingInstance = enclosingInstance; + this._filter = filter; + this._offset = 0; + this._filterLength = filter.Length; + } + + /// + /// Reads either an operator, or an attribute, whichever is + /// next in the filter string. + /// If the next component is an attribute, it is read and stored in the + /// attr field of this class which may be retrieved with getAttr() + /// and a -1 is returned. Otherwise, the int value of the operator read is + /// returned. + /// + /// + /// The op or attribute. + /// + /// Unexpected end. + public Int32 OpOrAttr { + get { + if(this._offset >= this._filterLength) { + throw new LdapException(LdapException.UnexpectedEnd, LdapStatusCode.FilterError); + } + + Int32 ret; + Int32 testChar = this._filter[this._offset]; + + switch(testChar) { + case '&': + this._offset++; + ret = (Int32)FilterOp.And; + break; + case '|': + this._offset++; + ret = (Int32)FilterOp.Or; + break; + case '!': + this._offset++; + ret = (Int32)FilterOp.Not; + break; + default: + if(this._filter.Substring(this._offset).StartsWith(":=")) { + throw new LdapException("Missing matching rule", LdapStatusCode.FilterError); + } + + if(this._filter.Substring(this._offset).StartsWith("::=") || + this._filter.Substring(this._offset).StartsWith(":::=")) { + throw new LdapException("DN and matching rule not specified", LdapStatusCode.FilterError); + } + + // get first component of 'item' (attr or :dn or :matchingrule) + const String delims = "=~<>()"; + StringBuilder sb = new StringBuilder(); + while(delims.IndexOf(this._filter[this._offset]) == -1 && + this._filter.Substring(this._offset).StartsWith(":=") == false) { + _ = sb.Append(this._filter[this._offset++]); + } + + this.Attr = sb.ToString().Trim(); + + // is there an attribute name specified in the filter ? + if(this.Attr.Length == 0 || this.Attr[0] == ';') { + throw new LdapException("Missing attribute description", LdapStatusCode.FilterError); + } + + Int32 index; + for(index = 0; index < this.Attr.Length; index++) { + Char atIndex = this.Attr[index]; + if(Char.IsLetterOrDigit(atIndex) || atIndex == '-' || atIndex == '.' || + atIndex == ';' || + atIndex == ':') { + continue; + } + + if(atIndex == '\\') { + throw new LdapException("Escape sequence not allowed in attribute description", + LdapStatusCode.FilterError); + } + + throw new LdapException($"Invalid character \"{atIndex}\" in attribute description", + LdapStatusCode.FilterError); + } + + // is there an option specified in the filter ? + index = this.Attr.IndexOf(';'); + if(index != -1 && index == this.Attr.Length - 1) { + throw new LdapException("Semicolon present, but no option specified", + LdapStatusCode.FilterError); + } + + ret = -1; + break; + } + + return ret; + } + } + + public FilterOp FilterType { + get { + if(this._offset >= this._filterLength) { + throw new LdapException(LdapException.UnexpectedEnd, LdapStatusCode.FilterError); + } + + if(this._filter.Substring(this._offset).StartsWith(">=")) { + this._offset += 2; + return FilterOp.GreaterOrEqual; + } + + if(this._filter.Substring(this._offset).StartsWith("<=")) { + this._offset += 2; + return FilterOp.LessOrEqual; + } + + if(this._filter.Substring(this._offset).StartsWith("~=")) { + this._offset += 2; + return FilterOp.ApproxMatch; + } + + if(this._filter.Substring(this._offset).StartsWith(":=")) { + this._offset += 2; + return FilterOp.ExtensibleMatch; + } + + if(this._filter[this._offset] == '=') { + this._offset++; + return FilterOp.EqualityMatch; + } + + throw new LdapException("Invalid comparison operator", LdapStatusCode.FilterError); + } + } + + public String Value { + get { + if(this._offset >= this._filterLength) { + throw new LdapException(LdapException.UnexpectedEnd, LdapStatusCode.FilterError); + } + + Int32 idx = this._filter.IndexOf(')', this._offset); + if(idx == -1) { + idx = this._filterLength; + } + + String ret = this._filter.Substring(this._offset, idx - this._offset); + this._offset = idx; + return ret; + } + } + + public String Attr { + get; private set; + } + + public RfcFilter EnclosingInstance { + get; + } + + public void GetLeftParen() { + if(this._offset >= this._filterLength) { + throw new LdapException(LdapException.UnexpectedEnd, LdapStatusCode.FilterError); + } + + if(this._filter[this._offset++] != '(') { + throw new LdapException(String.Format(LdapException.ExpectingLeftParen, this._filter[this._offset -= 1]), + LdapStatusCode.FilterError); + } + } + + public void GetRightParen() { + if(this._offset >= this._filterLength) { + throw new LdapException(LdapException.UnexpectedEnd, LdapStatusCode.FilterError); + } + + if(this._filter[this._offset++] != ')') { + throw new LdapException(String.Format(LdapException.ExpectingRightParen, this._filter[this._offset - 1]), + LdapStatusCode.FilterError); + } + } + + public Char PeekChar() { + if(this._offset >= this._filterLength) { + throw new LdapException(LdapException.UnexpectedEnd, LdapStatusCode.FilterError); + } + + return this._filter[this._offset]; + } + } + + /// + /// This inner class wrappers the Search Filter with an iterator. + /// This iterator will give access to all the individual components + /// pre-parsed. The first call to next will return an Integer identifying + /// the type of filter component. Then the component values will be returned + /// AND, NOT, and OR components values will be returned as Iterators. + /// + /// + private sealed class FilterIterator + : IEnumerator { + private readonly Asn1Tagged _root; + + private readonly RfcFilter _enclosingInstance; + + private Boolean _tagReturned; + + private Int32 _index = -1; + + private Boolean _hasMore = true; + + public FilterIterator(RfcFilter enclosingInstance, Asn1Tagged root) { + this._enclosingInstance = enclosingInstance; + this._root = root; + } + + public Object Current { + get { + Object toReturn = null; + + if(!this._tagReturned) { + this._tagReturned = true; + toReturn = this._root.GetIdentifier().Tag; + } else { + Asn1Object asn1 = this._root.TaggedValue; + + switch(asn1) { + case Asn1OctetString s: + + // one value to iterate + this._hasMore = false; + toReturn = s.StringValue(); + break; + case RfcSubstringFilter sub: + if(this._index == -1) { + // return attribute name + this._index = 0; + Asn1OctetString attr = (Asn1OctetString)sub.Get(0); + toReturn = attr.StringValue(); + } else if(this._index % 2 == 0) { + // return substring identifier + Asn1SequenceOf substrs = (Asn1SequenceOf)sub.Get(1); + toReturn = ((Asn1Tagged)substrs.Get(this._index / 2)).GetIdentifier().Tag; + this._index++; + } else { + // return substring value + Asn1SequenceOf substrs = (Asn1SequenceOf)sub.Get(1); + Asn1Tagged tag = (Asn1Tagged)substrs.Get(this._index / 2); + toReturn = ((Asn1OctetString)tag.TaggedValue).StringValue(); + this._index++; + } + + if(this._index / 2 >= ((Asn1SequenceOf)sub.Get(1)).Size()) { + this._hasMore = false; + } + + break; + case RfcAttributeValueAssertion assertion: + + // components: =,>=,<=,~= + if(this._index == -1) { + toReturn = assertion.AttributeDescription; + this._index = 1; + } else if(this._index == 1) { + toReturn = assertion.AssertionValue; + this._index = 2; + this._hasMore = false; + } + + break; + case RfcMatchingRuleAssertion exMatch: + + // Extensible match + if(this._index == -1) { + this._index = 0; + } + + toReturn = + ((Asn1OctetString)((Asn1Tagged)exMatch.Get(this._index++)).TaggedValue) + .StringValue(); + if(this._index > 2) { + this._hasMore = false; + } + + break; + case Asn1SetOf setRenamed: + + // AND and OR nested components + if(this._index == -1) { + this._index = 0; + } + + toReturn = new FilterIterator(this._enclosingInstance, + (Asn1Tagged)setRenamed.Get(this._index++)); + if(this._index >= setRenamed.Size()) { + this._hasMore = false; + } + + break; + case Asn1Tagged _: + + // NOT nested component. + toReturn = new FilterIterator(this._enclosingInstance, (Asn1Tagged)asn1); + this._hasMore = false; + break; + } + } + + return toReturn; + } + } + + public void Reset() { + // do nothing + } + + public Boolean MoveNext() => this._hasMore; + } + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/Ldap/RfcLdap.cs b/Unosquare.Swan/Networking/Ldap/RfcLdap.cs index 8d1f9e5..4aa3d3f 100644 --- a/Unosquare.Swan/Networking/Ldap/RfcLdap.cs +++ b/Unosquare.Swan/Networking/Ldap/RfcLdap.cs @@ -1,246 +1,238 @@ -namespace Unosquare.Swan.Networking.Ldap -{ - using System.IO; - +using System; +using System.IO; + +namespace Unosquare.Swan.Networking.Ldap { + /// + /// Encapsulates a single search result that is in response to an asynchronous + /// search operation. + /// + /// + internal class LdapSearchResult : LdapMessage { + private LdapEntry _entry; + + internal LdapSearchResult(RfcLdapMessage message) + : base(message) { + } + + public LdapEntry Entry { + get { + if(this._entry != null) { + return this._entry; + } + + LdapAttributeSet attrs = new LdapAttributeSet(); + RfcSearchResultEntry entry = (RfcSearchResultEntry)this.Message.Response; + + foreach(Asn1Object o in entry.Attributes.ToArray()) { + Asn1Sequence seq = (Asn1Sequence)o; + LdapAttribute attr = new LdapAttribute(((Asn1OctetString)seq.Get(0)).StringValue()); + Asn1Set set = (Asn1Set)seq.Get(1); + + foreach(Asn1Object t in set.ToArray()) { + attr.AddValue(((Asn1OctetString)t).ByteValue()); + } + + _ = attrs.Add(attr); + } + + this._entry = new LdapEntry(entry.ObjectName, attrs); + + return this._entry; + } + } + + public override String ToString() => this._entry?.ToString() ?? base.ToString(); + } + + /// + /// Represents an Ldap Search Result Reference. + ///
    +  /// SearchResultReference ::= [APPLICATION 19] SEQUENCE OF LdapURL
    +  /// 
    + ///
    + /// + internal class RfcSearchResultReference : Asn1SequenceOf { /// - /// Encapsulates a single search result that is in response to an asynchronous - /// search operation. + /// Initializes a new instance of the class. + /// The only time a client will create a SearchResultReference is when it is + /// decoding it from an Stream. /// - /// - internal class LdapSearchResult : LdapMessage - { - private LdapEntry _entry; - - internal LdapSearchResult(RfcLdapMessage message) - : base(message) - { - } - - public LdapEntry Entry - { - get - { - if (_entry != null) return _entry; - - var attrs = new LdapAttributeSet(); - var entry = (RfcSearchResultEntry) Message.Response; - - foreach (var o in entry.Attributes.ToArray()) - { - var seq = (Asn1Sequence) o; - var attr = new LdapAttribute(((Asn1OctetString)seq.Get(0)).StringValue()); - var set = (Asn1Set)seq.Get(1); - - foreach (var t in set.ToArray()) - { - attr.AddValue(((Asn1OctetString)t).ByteValue()); - } - - attrs.Add(attr); - } - - _entry = new LdapEntry(entry.ObjectName, attrs); - - return _entry; - } - } - - public override string ToString() => _entry?.ToString() ?? base.ToString(); - } - + /// The streab. + /// The length. + public RfcSearchResultReference(Stream stream, Int32 len) + : base(stream, len) { + } + + public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.SearchResultReference); + } + + /// + /// Represents an Ldap Extended Response. + ///
    +  /// ExtendedResponse ::= [APPLICATION 24] SEQUENCE {
    +  /// COMPONENTS OF LdapResult,
    +  /// responseName     [10] LdapOID OPTIONAL,
    +  /// response         [11] OCTET STRING OPTIONAL }
    +  /// 
    + ///
    + /// + /// + internal class RfcExtendedResponse : Asn1Sequence, IRfcResponse { + public const Int32 ResponseNameCode = 10; + + public const Int32 ResponseCode = 11; + + private readonly Int32 _referralIndex; + private readonly Int32 _responseNameIndex; + private readonly Int32 _responseIndex; + /// - /// Represents an Ldap Search Result Reference. - ///
    -    /// SearchResultReference ::= [APPLICATION 19] SEQUENCE OF LdapURL
    -    /// 
    + /// Initializes a new instance of the class. + /// The only time a client will create a ExtendedResponse is when it is + /// decoding it from an stream. ///
    - /// - internal class RfcSearchResultReference : Asn1SequenceOf - { - /// - /// Initializes a new instance of the class. - /// The only time a client will create a SearchResultReference is when it is - /// decoding it from an Stream. - /// - /// The streab. - /// The length. - public RfcSearchResultReference(Stream stream, int len) - : base(stream, len) - { - } - - public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.SearchResultReference); - } - + /// The stream. + /// The length. + public RfcExtendedResponse(Stream stream, Int32 len) + : base(stream, len) { + if(this.Size() <= 3) { + return; + } + + for(Int32 i = 3; i < this.Size(); i++) { + Asn1Tagged obj = (Asn1Tagged)this.Get(i); + Asn1Identifier id = obj.GetIdentifier(); + + switch(id.Tag) { + case RfcLdapResult.Referral: + SByte[] content = ((Asn1OctetString)obj.TaggedValue).ByteValue(); + + using(MemoryStream bais = new MemoryStream(content.ToByteArray())) { + this.Set(i, new Asn1SequenceOf(bais, content.Length)); + } + + this._referralIndex = i; + break; + case ResponseNameCode: + this.Set(i, new Asn1OctetString(((Asn1OctetString)obj.TaggedValue).ByteValue())); + this._responseNameIndex = i; + break; + case ResponseCode: + this.Set(i, obj.TaggedValue); + this._responseIndex = i; + break; + } + } + } + + public Asn1OctetString ResponseName => this._responseNameIndex != 0 ? (Asn1OctetString)this.Get(this._responseNameIndex) : null; + + public Asn1OctetString Response => this._responseIndex != 0 ? (Asn1OctetString)this.Get(this._responseIndex) : null; + + public Asn1Enumerated GetResultCode() => (Asn1Enumerated)this.Get(0); + + public Asn1OctetString GetMatchedDN() => new Asn1OctetString(((Asn1OctetString)this.Get(1)).ByteValue()); + + public Asn1OctetString GetErrorMessage() => new Asn1OctetString(((Asn1OctetString)this.Get(2)).ByteValue()); + + public Asn1SequenceOf GetReferral() + => this._referralIndex != 0 ? (Asn1SequenceOf)this.Get(this._referralIndex) : null; + + public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.ExtendedResponse); + } + + /// + /// Represents and Ldap Bind Response. + ///
    +  /// BindResponse ::= [APPLICATION 1] SEQUENCE {
    +  /// COMPONENTS OF LdapResult,
    +  /// serverSaslCreds    [7] OCTET STRING OPTIONAL }
    +  /// 
    + ///
    + /// + /// + internal class RfcBindResponse : Asn1Sequence, IRfcResponse { /// - /// Represents an Ldap Extended Response. - ///
    -    /// ExtendedResponse ::= [APPLICATION 24] SEQUENCE {
    -    /// COMPONENTS OF LdapResult,
    -    /// responseName     [10] LdapOID OPTIONAL,
    -    /// response         [11] OCTET STRING OPTIONAL }
    -    /// 
    + /// Initializes a new instance of the class. + /// The only time a client will create a BindResponse is when it is + /// decoding it from an InputStream + /// Note: If serverSaslCreds is included in the BindResponse, it does not + /// need to be decoded since it is already an OCTET STRING. ///
    - /// - /// - internal class RfcExtendedResponse : Asn1Sequence, IRfcResponse - { - public const int ResponseNameCode = 10; - - public const int ResponseCode = 11; - - private readonly int _referralIndex; - private readonly int _responseNameIndex; - private readonly int _responseIndex; - - /// - /// Initializes a new instance of the class. - /// The only time a client will create a ExtendedResponse is when it is - /// decoding it from an stream. - /// - /// The stream. - /// The length. - public RfcExtendedResponse(Stream stream, int len) - : base(stream, len) - { - if (Size() <= 3) return; - - for (var i = 3; i < Size(); i++) - { - var obj = (Asn1Tagged) Get(i); - var id = obj.GetIdentifier(); - - switch (id.Tag) - { - case RfcLdapResult.Referral: - var content = ((Asn1OctetString) obj.TaggedValue).ByteValue(); - - using (var bais = new MemoryStream(content.ToByteArray())) - Set(i, new Asn1SequenceOf(bais, content.Length)); - - _referralIndex = i; - break; - case ResponseNameCode: - Set(i, new Asn1OctetString(((Asn1OctetString) obj.TaggedValue).ByteValue())); - _responseNameIndex = i; - break; - case ResponseCode: - Set(i, obj.TaggedValue); - _responseIndex = i; - break; - } - } - } - - public Asn1OctetString ResponseName => _responseNameIndex != 0 ? (Asn1OctetString) Get(_responseNameIndex) : null; - - public Asn1OctetString Response => _responseIndex != 0 ? (Asn1OctetString) Get(_responseIndex) : null; - - public Asn1Enumerated GetResultCode() => (Asn1Enumerated) Get(0); - - public Asn1OctetString GetMatchedDN() => new Asn1OctetString(((Asn1OctetString) Get(1)).ByteValue()); - - public Asn1OctetString GetErrorMessage() => new Asn1OctetString(((Asn1OctetString) Get(2)).ByteValue()); - - public Asn1SequenceOf GetReferral() - => _referralIndex != 0 ? (Asn1SequenceOf) Get(_referralIndex) : null; - - public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.ExtendedResponse); - } - - /// - /// Represents and Ldap Bind Response. - ///
    -    /// BindResponse ::= [APPLICATION 1] SEQUENCE {
    -    /// COMPONENTS OF LdapResult,
    -    /// serverSaslCreds    [7] OCTET STRING OPTIONAL }
    -    /// 
    - ///
    - /// - /// - internal class RfcBindResponse : Asn1Sequence, IRfcResponse - { - /// - /// Initializes a new instance of the class. - /// The only time a client will create a BindResponse is when it is - /// decoding it from an InputStream - /// Note: If serverSaslCreds is included in the BindResponse, it does not - /// need to be decoded since it is already an OCTET STRING. - /// - /// The in renamed. - /// The length. - public RfcBindResponse(Stream stream, int len) - : base(stream, len) - { - // Decode optional referral from Asn1OctetString to Referral. - if (Size() <= 3) return; - - var obj = (Asn1Tagged) Get(3); - - if (obj.GetIdentifier().Tag != RfcLdapResult.Referral) return; - - var content = ((Asn1OctetString) obj.TaggedValue).ByteValue(); - - using (var bais = new MemoryStream(content.ToByteArray())) - Set(3, new Asn1SequenceOf(bais, content.Length)); - } - - public Asn1Enumerated GetResultCode() => (Asn1Enumerated) Get(0); - - public Asn1OctetString GetMatchedDN() => new Asn1OctetString(((Asn1OctetString) Get(1)).ByteValue()); - - public Asn1OctetString GetErrorMessage() => new Asn1OctetString(((Asn1OctetString) Get(2)).ByteValue()); - - public Asn1SequenceOf GetReferral() => Size() > 3 && Get(3) is Asn1SequenceOf ? (Asn1SequenceOf) Get(3) : null; - - public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.BindResponse); - } - - /// - /// Represents an LDAP Intermediate Response. - /// IntermediateResponse ::= [APPLICATION 25] SEQUENCE { - /// COMPONENTS OF LDAPResult, note: only present on incorrectly - /// encoded response from pre Falcon-sp1 server - /// responseName [10] LDAPOID OPTIONAL, - /// responseValue [11] OCTET STRING OPTIONAL }. - /// - /// - /// - internal class RfcIntermediateResponse : Asn1Sequence, IRfcResponse - { - public const int TagResponseName = 0; - public const int TagResponse = 1; - - public RfcIntermediateResponse(Stream stream, int len) - : base(stream, len) - { - var i = Size() >= 3 ? 3 : 0; - - for (; i < Size(); i++) - { - var obj = (Asn1Tagged) Get(i); - - switch (obj.GetIdentifier().Tag) - { - case TagResponseName: - Set(i, new Asn1OctetString(((Asn1OctetString) obj.TaggedValue).ByteValue())); - break; - case TagResponse: - Set(i, obj.TaggedValue); - break; - } - } - } - - public Asn1Enumerated GetResultCode() => Size() > 3 ? (Asn1Enumerated) Get(0) : null; - - public Asn1OctetString GetMatchedDN() => Size() > 3 ? new Asn1OctetString(((Asn1OctetString) Get(1)).ByteValue()) : null; - - public Asn1OctetString GetErrorMessage() => - Size() > 3 ? new Asn1OctetString(((Asn1OctetString) Get(2)).ByteValue()) : null; - - public Asn1SequenceOf GetReferral() => Size() > 3 ? (Asn1SequenceOf) Get(3) : null; - - public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.IntermediateResponse); - } + /// The in renamed. + /// The length. + public RfcBindResponse(Stream stream, Int32 len) + : base(stream, len) { + // Decode optional referral from Asn1OctetString to Referral. + if(this.Size() <= 3) { + return; + } + + Asn1Tagged obj = (Asn1Tagged)this.Get(3); + + if(obj.GetIdentifier().Tag != RfcLdapResult.Referral) { + return; + } + + SByte[] content = ((Asn1OctetString)obj.TaggedValue).ByteValue(); + + using(MemoryStream bais = new MemoryStream(content.ToByteArray())) { + this.Set(3, new Asn1SequenceOf(bais, content.Length)); + } + } + + public Asn1Enumerated GetResultCode() => (Asn1Enumerated)this.Get(0); + + public Asn1OctetString GetMatchedDN() => new Asn1OctetString(((Asn1OctetString)this.Get(1)).ByteValue()); + + public Asn1OctetString GetErrorMessage() => new Asn1OctetString(((Asn1OctetString)this.Get(2)).ByteValue()); + + public Asn1SequenceOf GetReferral() => this.Size() > 3 && this.Get(3) is Asn1SequenceOf ? (Asn1SequenceOf)this.Get(3) : null; + + public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.BindResponse); + } + + /// + /// Represents an LDAP Intermediate Response. + /// IntermediateResponse ::= [APPLICATION 25] SEQUENCE { + /// COMPONENTS OF LDAPResult, note: only present on incorrectly + /// encoded response from pre Falcon-sp1 server + /// responseName [10] LDAPOID OPTIONAL, + /// responseValue [11] OCTET STRING OPTIONAL }. + /// + /// + /// + internal class RfcIntermediateResponse : Asn1Sequence, IRfcResponse { + public const Int32 TagResponseName = 0; + public const Int32 TagResponse = 1; + + public RfcIntermediateResponse(Stream stream, Int32 len) + : base(stream, len) { + Int32 i = this.Size() >= 3 ? 3 : 0; + + for(; i < this.Size(); i++) { + Asn1Tagged obj = (Asn1Tagged)this.Get(i); + + switch(obj.GetIdentifier().Tag) { + case TagResponseName: + this.Set(i, new Asn1OctetString(((Asn1OctetString)obj.TaggedValue).ByteValue())); + break; + case TagResponse: + this.Set(i, obj.TaggedValue); + break; + } + } + } + + public Asn1Enumerated GetResultCode() => this.Size() > 3 ? (Asn1Enumerated)this.Get(0) : null; + + public Asn1OctetString GetMatchedDN() => this.Size() > 3 ? new Asn1OctetString(((Asn1OctetString)this.Get(1)).ByteValue()) : null; + + public Asn1OctetString GetErrorMessage() => + this.Size() > 3 ? new Asn1OctetString(((Asn1OctetString)this.Get(2)).ByteValue()) : null; + + public Asn1SequenceOf GetReferral() => this.Size() > 3 ? (Asn1SequenceOf)this.Get(3) : null; + + public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.IntermediateResponse); + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/Ldap/RfcLdapMessage.cs b/Unosquare.Swan/Networking/Ldap/RfcLdapMessage.cs index 891b4d8..71fb237 100644 --- a/Unosquare.Swan/Networking/Ldap/RfcLdapMessage.cs +++ b/Unosquare.Swan/Networking/Ldap/RfcLdapMessage.cs @@ -1,390 +1,379 @@ -namespace Unosquare.Swan.Networking.Ldap -{ - using System; - using System.IO; - +using System; +using System.IO; + +namespace Unosquare.Swan.Networking.Ldap { + /// + /// Represents an Ldap Message. + ///
    +  /// LdapMessage ::= SEQUENCE {
    +  /// messageID       MessageID,
    +  /// protocolOp      CHOICE {
    +  /// bindRequest     BindRequest,
    +  /// bindResponse    BindResponse,
    +  /// unbindRequest   UnbindRequest,
    +  /// searchRequest   SearchRequest,
    +  /// searchResEntry  SearchResultEntry,
    +  /// searchResDone   SearchResultDone,
    +  /// searchResRef    SearchResultReference,
    +  /// modifyRequest   ModifyRequest,
    +  /// modifyResponse  ModifyResponse,
    +  /// addRequest      AddRequest,
    +  /// addResponse     AddResponse,
    +  /// delRequest      DelRequest,
    +  /// delResponse     DelResponse,
    +  /// modDNRequest    ModifyDNRequest,
    +  /// modDNResponse   ModifyDNResponse,
    +  /// compareRequest  CompareRequest,
    +  /// compareResponse CompareResponse,
    +  /// abandonRequest  AbandonRequest,
    +  /// extendedReq     ExtendedRequest,
    +  /// extendedResp    ExtendedResponse },
    +  /// controls       [0] Controls OPTIONAL }
    +  /// 
    + /// Note: The creation of a MessageID should be hidden within the creation of + /// an RfcLdapMessage. The MessageID needs to be in sequence, and has an + /// upper and lower limit. There is never a case when a user should be + /// able to specify the MessageID for an RfcLdapMessage. The MessageID() + /// constructor should be package protected. (So the MessageID value + /// isn't arbitrarily run up.). + ///
    + /// + internal sealed class RfcLdapMessage : Asn1Sequence { + private readonly Asn1Object _op; + /// - /// Represents an Ldap Message. - ///
    -    /// LdapMessage ::= SEQUENCE {
    -    /// messageID       MessageID,
    -    /// protocolOp      CHOICE {
    -    /// bindRequest     BindRequest,
    -    /// bindResponse    BindResponse,
    -    /// unbindRequest   UnbindRequest,
    -    /// searchRequest   SearchRequest,
    -    /// searchResEntry  SearchResultEntry,
    -    /// searchResDone   SearchResultDone,
    -    /// searchResRef    SearchResultReference,
    -    /// modifyRequest   ModifyRequest,
    -    /// modifyResponse  ModifyResponse,
    -    /// addRequest      AddRequest,
    -    /// addResponse     AddResponse,
    -    /// delRequest      DelRequest,
    -    /// delResponse     DelResponse,
    -    /// modDNRequest    ModifyDNRequest,
    -    /// modDNResponse   ModifyDNResponse,
    -    /// compareRequest  CompareRequest,
    -    /// compareResponse CompareResponse,
    -    /// abandonRequest  AbandonRequest,
    -    /// extendedReq     ExtendedRequest,
    -    /// extendedResp    ExtendedResponse },
    -    /// controls       [0] Controls OPTIONAL }
    -    /// 
    - /// Note: The creation of a MessageID should be hidden within the creation of - /// an RfcLdapMessage. The MessageID needs to be in sequence, and has an - /// upper and lower limit. There is never a case when a user should be - /// able to specify the MessageID for an RfcLdapMessage. The MessageID() - /// constructor should be package protected. (So the MessageID value - /// isn't arbitrarily run up.). + /// Initializes a new instance of the class. + /// Create an RfcLdapMessage request from input parameters. ///
    - /// - internal sealed class RfcLdapMessage : Asn1Sequence - { - private readonly Asn1Object _op; - - /// - /// Initializes a new instance of the class. - /// Create an RfcLdapMessage request from input parameters. - /// - /// The op. - /// The controls. - public RfcLdapMessage(IRfcRequest op, RfcControls controls) - : base(3) - { - _op = (Asn1Object) op; - - Add(new RfcMessageID()); // MessageID has static counter - Add((Asn1Object) op); - if (controls != null) - { - Add(controls); - } - } - - /// - /// Initializes a new instance of the class. - /// Will decode an RfcLdapMessage directly from an InputStream. - /// - /// The stream. - /// The length. - /// RfcLdapMessage: Invalid tag: " + protocolOpId.Tag. - public RfcLdapMessage(Stream stream, int len) - : base(stream, len) - { - // Decode implicitly tagged protocol operation from an Asn1Tagged type - // to its appropriate application type. - var protocolOp = (Asn1Tagged) Get(1); - var protocolOpId = protocolOp.GetIdentifier(); - var content = ((Asn1OctetString) protocolOp.TaggedValue).ByteValue(); - var bais = new MemoryStream(content.ToByteArray()); - - switch ((LdapOperation) protocolOpId.Tag) - { - case LdapOperation.SearchResponse: - Set(1, new RfcSearchResultEntry(bais, content.Length)); - break; - - case LdapOperation.SearchResult: - Set(1, new RfcSearchResultDone(bais, content.Length)); - break; - - case LdapOperation.SearchResultReference: - Set(1, new RfcSearchResultReference(bais, content.Length)); - break; - - case LdapOperation.BindResponse: - Set(1, new RfcBindResponse(bais, content.Length)); - break; - - case LdapOperation.ExtendedResponse: - Set(1, new RfcExtendedResponse(bais, content.Length)); - break; - - case LdapOperation.IntermediateResponse: - Set(1, new RfcIntermediateResponse(bais, content.Length)); - break; - case LdapOperation.ModifyResponse: - Set(1, new RfcModifyResponse(bais, content.Length)); - break; - - default: - throw new InvalidOperationException($"RfcLdapMessage: Invalid tag: {protocolOpId.Tag}"); - } - - // decode optional implicitly tagged controls from Asn1Tagged type to - // to RFC 2251 types. - if (Size() <= 2) return; - - var controls = (Asn1Tagged) Get(2); - content = ((Asn1OctetString) controls.TaggedValue).ByteValue(); - - using (var ms = new MemoryStream(content.ToByteArray())) - Set(2, new RfcControls(ms, content.Length)); - } - - public int MessageId => ((Asn1Integer) Get(0)).IntValue(); - - /// Returns this RfcLdapMessage's message type. - public LdapOperation Type => (LdapOperation) Get(1).GetIdentifier().Tag; - - public Asn1Object Response => Get(1); - - public string RequestDn => ((IRfcRequest) _op).GetRequestDN(); - - public LdapMessage RequestingMessage { get; set; } - - public IRfcRequest GetRequest() => (IRfcRequest) Get(1); - - public bool IsRequest() => Get(1) is IRfcRequest; - } - + /// The op. + /// The controls. + public RfcLdapMessage(IRfcRequest op, RfcControls controls) + : base(3) { + this._op = (Asn1Object)op; + + this.Add(new RfcMessageID()); // MessageID has static counter + this.Add((Asn1Object)op); + if(controls != null) { + this.Add(controls); + } + } + + /// - /// Represents Ldap Controls. - ///
    -    /// Controls ::= SEQUENCE OF Control
    -    /// 
    + /// Initializes a new instance of the class. + /// Will decode an RfcLdapMessage directly from an InputStream. ///
    - /// - internal class RfcControls : Asn1SequenceOf - { - public const int Controls = 0; - - public RfcControls() - : base(5) - { - } - - public RfcControls(Stream stream, int len) - : base(stream, len) - { - // Convert each SEQUENCE element to a Control - for (var i = 0; i < Size(); i++) - { - var tempControl = new RfcControl((Asn1Sequence) Get(i)); - Set(i, tempControl); - } - } - - public void Add(RfcControl control) => base.Add(control); - - public void Set(int index, RfcControl control) => base.Set(index, control); - - public override Asn1Identifier GetIdentifier() => new Asn1Identifier(Controls, true); - } - + /// The stream. + /// The length. + /// RfcLdapMessage: Invalid tag: " + protocolOpId.Tag. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0068:Empfohlenes Dispose-Muster verwenden", Justification = "")] + public RfcLdapMessage(Stream stream, Int32 len) + : base(stream, len) { + // Decode implicitly tagged protocol operation from an Asn1Tagged type + // to its appropriate application type. + Asn1Tagged protocolOp = (Asn1Tagged)this.Get(1); + Asn1Identifier protocolOpId = protocolOp.GetIdentifier(); + SByte[] content = ((Asn1OctetString)protocolOp.TaggedValue).ByteValue(); + MemoryStream bais = new MemoryStream(content.ToByteArray()); + + switch((LdapOperation)protocolOpId.Tag) { + case LdapOperation.SearchResponse: + this.Set(1, new RfcSearchResultEntry(bais, content.Length)); + break; + + case LdapOperation.SearchResult: + this.Set(1, new RfcSearchResultDone(bais, content.Length)); + break; + + case LdapOperation.SearchResultReference: + this.Set(1, new RfcSearchResultReference(bais, content.Length)); + break; + + case LdapOperation.BindResponse: + this.Set(1, new RfcBindResponse(bais, content.Length)); + break; + + case LdapOperation.ExtendedResponse: + this.Set(1, new RfcExtendedResponse(bais, content.Length)); + break; + + case LdapOperation.IntermediateResponse: + this.Set(1, new RfcIntermediateResponse(bais, content.Length)); + break; + case LdapOperation.ModifyResponse: + this.Set(1, new RfcModifyResponse(bais, content.Length)); + break; + + default: + throw new InvalidOperationException($"RfcLdapMessage: Invalid tag: {protocolOpId.Tag}"); + } + + // decode optional implicitly tagged controls from Asn1Tagged type to + // to RFC 2251 types. + if(this.Size() <= 2) { + return; + } + + Asn1Tagged controls = (Asn1Tagged)this.Get(2); + content = ((Asn1OctetString)controls.TaggedValue).ByteValue(); + + using(MemoryStream ms = new MemoryStream(content.ToByteArray())) { + this.Set(2, new RfcControls(ms, content.Length)); + } + } + + public Int32 MessageId => ((Asn1Integer)this.Get(0)).IntValue(); + + /// Returns this RfcLdapMessage's message type. + public LdapOperation Type => (LdapOperation)this.Get(1).GetIdentifier().Tag; + + public Asn1Object Response => this.Get(1); + + public String RequestDn => ((IRfcRequest)this._op).GetRequestDN(); + + public LdapMessage RequestingMessage { + get; set; + } + + public IRfcRequest GetRequest() => (IRfcRequest)this.Get(1); + + public Boolean IsRequest() => this.Get(1) is IRfcRequest; + } + + /// + /// Represents Ldap Controls. + ///
    +  /// Controls ::= SEQUENCE OF Control
    +  /// 
    + ///
    + /// + internal class RfcControls : Asn1SequenceOf { + public const Int32 Controls = 0; + + public RfcControls() + : base(5) { + } + + public RfcControls(Stream stream, Int32 len) + : base(stream, len) { + // Convert each SEQUENCE element to a Control + for(Int32 i = 0; i < this.Size(); i++) { + RfcControl tempControl = new RfcControl((Asn1Sequence)this.Get(i)); + this.Set(i, tempControl); + } + } + + public void Add(RfcControl control) => base.Add(control); + + public void Set(Int32 index, RfcControl control) => base.Set(index, control); + + public override Asn1Identifier GetIdentifier() => new Asn1Identifier(Controls, true); + } + + /// + /// This interface represents RfcLdapMessages that contain a response from a + /// server. + /// If the protocol operation of the RfcLdapMessage is of this type, + /// it contains at least an RfcLdapResult. + /// + internal interface IRfcResponse { /// - /// This interface represents RfcLdapMessages that contain a response from a - /// server. - /// If the protocol operation of the RfcLdapMessage is of this type, - /// it contains at least an RfcLdapResult. + /// Gets the result code. /// - internal interface IRfcResponse - { - /// - /// Gets the result code. - /// - /// Asn1Enumerated. - Asn1Enumerated GetResultCode(); - - /// - /// Gets the matched dn. - /// - /// RfcLdapDN. - Asn1OctetString GetMatchedDN(); - - /// - /// Gets the error message. - /// - /// RfcLdapString. - Asn1OctetString GetErrorMessage(); - - /// - /// Gets the referral. - /// - /// Asn1SequenceOf. - Asn1SequenceOf GetReferral(); - } - + /// Asn1Enumerated. + Asn1Enumerated GetResultCode(); + /// - /// This interface represents Protocol Operations that are requests from a - /// client. + /// Gets the matched dn. /// - internal interface IRfcRequest - { - /// - /// Builds a new request using the data from the this object. - /// - /// A . - string GetRequestDN(); - } - + /// RfcLdapDN. + Asn1OctetString GetMatchedDN(); + /// - /// Represents an LdapResult. - ///
    -    /// LdapResult ::= SEQUENCE {
    -    /// resultCode      ENUMERATED {
    -    /// success                      (0),
    -    /// operationsError              (1),
    -    /// protocolError                (2),
    -    /// timeLimitExceeded            (3),
    -    /// sizeLimitExceeded            (4),
    -    /// compareFalse                 (5),
    -    /// compareTrue                  (6),
    -    /// authMethodNotSupported       (7),
    -    /// strongAuthRequired           (8),
    -    /// -- 9 reserved --
    -    /// referral                     (10),  -- new
    -    /// adminLimitExceeded           (11),  -- new
    -    /// unavailableCriticalExtension (12),  -- new
    -    /// confidentialityRequired      (13),  -- new
    -    /// saslBindInProgress           (14),  -- new
    -    /// noSuchAttribute              (16),
    -    /// undefinedAttributeType       (17),
    -    /// inappropriateMatching        (18),
    -    /// constraintViolation          (19),
    -    /// attributeOrValueExists       (20),
    -    /// invalidAttributeSyntax       (21),
    -    /// -- 22-31 unused --
    -    /// noSuchObject                 (32),
    -    /// aliasProblem                 (33),
    -    /// invalidDNSyntax              (34),
    -    /// -- 35 reserved for undefined isLeaf --
    -    /// aliasDereferencingProblem    (36),
    -    /// -- 37-47 unused --
    -    /// inappropriateAuthentication  (48),
    -    /// invalidCredentials           (49),
    -    /// insufficientAccessRights     (50),
    -    /// busy                         (51),
    -    /// unavailable                  (52),
    -    /// unwillingToPerform           (53),
    -    /// loopDetect                   (54),
    -    /// -- 55-63 unused --
    -    /// namingViolation              (64),
    -    /// objectClassViolation         (65),
    -    /// notAllowedOnNonLeaf          (66),
    -    /// notAllowedOnRDN              (67),
    -    /// entryAlreadyExists           (68),
    -    /// objectClassModsProhibited    (69),
    -    /// -- 70 reserved for CLdap --
    -    /// affectsMultipleDSAs          (71), -- new
    -    /// -- 72-79 unused --
    -    /// other                        (80) },
    -    /// -- 81-90 reserved for APIs --
    -    /// matchedDN       LdapDN,
    -    /// errorMessage    LdapString,
    -    /// referral        [3] Referral OPTIONAL }
    -    /// 
    + /// Gets the error message. ///
    - /// - /// - internal class RfcLdapResult : Asn1Sequence, IRfcResponse - { - public const int Referral = 3; - - public RfcLdapResult(Stream stream, int len) - : base(stream, len) - { - // Decode optional referral from Asn1OctetString to Referral. - if (Size() <= 3) return; - - var obj = (Asn1Tagged) Get(3); - var id = obj.GetIdentifier(); - - if (id.Tag != Referral) return; - - var content = ((Asn1OctetString) obj.TaggedValue).ByteValue(); - Set(3, new Asn1SequenceOf(new MemoryStream(content.ToByteArray()), content.Length)); - } - - public Asn1Enumerated GetResultCode() => (Asn1Enumerated) Get(0); - - public Asn1OctetString GetMatchedDN() => new Asn1OctetString(((Asn1OctetString) Get(1)).ByteValue()); - - public Asn1OctetString GetErrorMessage() => new Asn1OctetString(((Asn1OctetString) Get(2)).ByteValue()); - - public Asn1SequenceOf GetReferral() => Size() > 3 ? (Asn1SequenceOf) Get(3) : null; - } - + /// RfcLdapString. + Asn1OctetString GetErrorMessage(); + /// - /// Represents an Ldap Search Result Done Response. - ///
    -    /// SearchResultDone ::= [APPLICATION 5] LdapResult
    -    /// 
    + /// Gets the referral. ///
    - /// - internal class RfcSearchResultDone : RfcLdapResult - { - public RfcSearchResultDone(Stream stream, int len) - : base(stream, len) - { - } - - public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.SearchResult); - } - + /// Asn1SequenceOf. + Asn1SequenceOf GetReferral(); + } + + /// + /// This interface represents Protocol Operations that are requests from a + /// client. + /// + internal interface IRfcRequest { /// - /// Represents an Ldap Search Result Entry. - ///
    -    /// SearchResultEntry ::= [APPLICATION 4] SEQUENCE {
    -    /// objectName      LdapDN,
    -    /// attributes      PartialAttributeList }
    -    /// 
    + /// Builds a new request using the data from the this object. ///
    - /// - internal sealed class RfcSearchResultEntry : Asn1Sequence - { - public RfcSearchResultEntry(Stream stream, int len) - : base(stream, len) - { - } - - public string ObjectName => ((Asn1OctetString) Get(0)).StringValue(); - - public Asn1Sequence Attributes => (Asn1Sequence) Get(1); - - public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.SearchResponse); - } - + /// A . + String GetRequestDN(); + } + + /// + /// Represents an LdapResult. + ///
    +  /// LdapResult ::= SEQUENCE {
    +  /// resultCode      ENUMERATED {
    +  /// success                      (0),
    +  /// operationsError              (1),
    +  /// protocolError                (2),
    +  /// timeLimitExceeded            (3),
    +  /// sizeLimitExceeded            (4),
    +  /// compareFalse                 (5),
    +  /// compareTrue                  (6),
    +  /// authMethodNotSupported       (7),
    +  /// strongAuthRequired           (8),
    +  /// -- 9 reserved --
    +  /// referral                     (10),  -- new
    +  /// adminLimitExceeded           (11),  -- new
    +  /// unavailableCriticalExtension (12),  -- new
    +  /// confidentialityRequired      (13),  -- new
    +  /// saslBindInProgress           (14),  -- new
    +  /// noSuchAttribute              (16),
    +  /// undefinedAttributeType       (17),
    +  /// inappropriateMatching        (18),
    +  /// constraintViolation          (19),
    +  /// attributeOrValueExists       (20),
    +  /// invalidAttributeSyntax       (21),
    +  /// -- 22-31 unused --
    +  /// noSuchObject                 (32),
    +  /// aliasProblem                 (33),
    +  /// invalidDNSyntax              (34),
    +  /// -- 35 reserved for undefined isLeaf --
    +  /// aliasDereferencingProblem    (36),
    +  /// -- 37-47 unused --
    +  /// inappropriateAuthentication  (48),
    +  /// invalidCredentials           (49),
    +  /// insufficientAccessRights     (50),
    +  /// busy                         (51),
    +  /// unavailable                  (52),
    +  /// unwillingToPerform           (53),
    +  /// loopDetect                   (54),
    +  /// -- 55-63 unused --
    +  /// namingViolation              (64),
    +  /// objectClassViolation         (65),
    +  /// notAllowedOnNonLeaf          (66),
    +  /// notAllowedOnRDN              (67),
    +  /// entryAlreadyExists           (68),
    +  /// objectClassModsProhibited    (69),
    +  /// -- 70 reserved for CLdap --
    +  /// affectsMultipleDSAs          (71), -- new
    +  /// -- 72-79 unused --
    +  /// other                        (80) },
    +  /// -- 81-90 reserved for APIs --
    +  /// matchedDN       LdapDN,
    +  /// errorMessage    LdapString,
    +  /// referral        [3] Referral OPTIONAL }
    +  /// 
    + ///
    + /// + /// + internal class RfcLdapResult : Asn1Sequence, IRfcResponse { + public const Int32 Referral = 3; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0068:Empfohlenes Dispose-Muster verwenden", Justification = "")] + public RfcLdapResult(Stream stream, Int32 len) + : base(stream, len) { + // Decode optional referral from Asn1OctetString to Referral. + if(this.Size() <= 3) { + return; + } + + Asn1Tagged obj = (Asn1Tagged)this.Get(3); + Asn1Identifier id = obj.GetIdentifier(); + + if(id.Tag != Referral) { + return; + } + + SByte[] content = ((Asn1OctetString)obj.TaggedValue).ByteValue(); + this.Set(3, new Asn1SequenceOf(new MemoryStream(content.ToByteArray()), content.Length)); + } + + public Asn1Enumerated GetResultCode() => (Asn1Enumerated)this.Get(0); + + public Asn1OctetString GetMatchedDN() => new Asn1OctetString(((Asn1OctetString)this.Get(1)).ByteValue()); + + public Asn1OctetString GetErrorMessage() => new Asn1OctetString(((Asn1OctetString)this.Get(2)).ByteValue()); + + public Asn1SequenceOf GetReferral() => this.Size() > 3 ? (Asn1SequenceOf)this.Get(3) : null; + } + + /// + /// Represents an Ldap Search Result Done Response. + ///
    +  /// SearchResultDone ::= [APPLICATION 5] LdapResult
    +  /// 
    + ///
    + /// + internal class RfcSearchResultDone : RfcLdapResult { + public RfcSearchResultDone(Stream stream, Int32 len) + : base(stream, len) { + } + + public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.SearchResult); + } + + /// + /// Represents an Ldap Search Result Entry. + ///
    +  /// SearchResultEntry ::= [APPLICATION 4] SEQUENCE {
    +  /// objectName      LdapDN,
    +  /// attributes      PartialAttributeList }
    +  /// 
    + ///
    + /// + internal sealed class RfcSearchResultEntry : Asn1Sequence { + public RfcSearchResultEntry(Stream stream, Int32 len) + : base(stream, len) { + } + + public String ObjectName => ((Asn1OctetString)this.Get(0)).StringValue(); + + public Asn1Sequence Attributes => (Asn1Sequence)this.Get(1); + + public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.SearchResponse); + } + + /// + /// Represents an Ldap Message ID. + ///
    +  /// MessageID ::= INTEGER (0 .. maxInt)
    +  /// maxInt INTEGER ::= 2147483647 -- (2^^31 - 1) --
    +  /// Note: The creation of a MessageID should be hidden within the creation of
    +  /// an RfcLdapMessage. The MessageID needs to be in sequence, and has an
    +  /// upper and lower limit. There is never a case when a user should be
    +  /// able to specify the MessageID for an RfcLdapMessage. The MessageID()
    +  /// class should be package protected. (So the MessageID value isn't
    +  /// arbitrarily run up.)
    +  /// 
    + /// + internal class RfcMessageID : Asn1Integer { + private static Int32 _messageId; + private static readonly Object SyncRoot = new Object(); + /// - /// Represents an Ldap Message ID. - ///
    -    /// MessageID ::= INTEGER (0 .. maxInt)
    -    /// maxInt INTEGER ::= 2147483647 -- (2^^31 - 1) --
    -    /// Note: The creation of a MessageID should be hidden within the creation of
    -    /// an RfcLdapMessage. The MessageID needs to be in sequence, and has an
    -    /// upper and lower limit. There is never a case when a user should be
    -    /// able to specify the MessageID for an RfcLdapMessage. The MessageID()
    -    /// class should be package protected. (So the MessageID value isn't
    -    /// arbitrarily run up.)
    -    /// 
    - /// - internal class RfcMessageID : Asn1Integer - { - private static int _messageId; - private static readonly object SyncRoot = new object(); - - /// - /// Initializes a new instance of the class. - /// Creates a MessageID with an auto incremented Asn1Integer value. - /// Bounds: (0 .. 2,147,483,647) (2^^31 - 1 or Integer.MAX_VALUE) - /// MessageID zero is never used in this implementation. Always - /// start the messages with one. - /// - protected internal RfcMessageID() - : base(MessageId) - { - } - - private static int MessageId - { - get - { - lock (SyncRoot) - { - return _messageId < int.MaxValue ? ++_messageId : (_messageId = 1); - } - } - } - } + /// Initializes a new instance of the class. + /// Creates a MessageID with an auto incremented Asn1Integer value. + /// Bounds: (0 .. 2,147,483,647) (2^^31 - 1 or Integer.MAX_VALUE) + /// MessageID zero is never used in this implementation. Always + /// start the messages with one. + /// + protected internal RfcMessageID() + : base(MessageId) { + } + + private static Int32 MessageId { + get { + lock(SyncRoot) { + return _messageId < Int32.MaxValue ? ++_messageId : (_messageId = 1); + } + } + } + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/Ldap/RfcModifyRequest.cs b/Unosquare.Swan/Networking/Ldap/RfcModifyRequest.cs index 8f68497..cf212df 100644 --- a/Unosquare.Swan/Networking/Ldap/RfcModifyRequest.cs +++ b/Unosquare.Swan/Networking/Ldap/RfcModifyRequest.cs @@ -1,46 +1,42 @@ -namespace Unosquare.Swan.Networking.Ldap -{ - using System.IO; - - /// - /// Represents an Ldap Modify Request. - ///
    -    /// ModifyRequest ::= [APPLICATION 6] SEQUENCE {
    -    /// object          LdapDN,
    -    /// modification    SEQUENCE OF SEQUENCE {
    -    /// operation       ENUMERATED {
    -    /// add     (0),
    -    /// delete  (1),
    -    /// replace (2) },
    -    /// modification    AttributeTypeAndValues } }
    -    /// 
    - ///
    - /// - /// - internal sealed class RfcModifyRequest - : Asn1Sequence, IRfcRequest - { - public RfcModifyRequest(string obj, Asn1SequenceOf modification) - : base(2) - { - Add(obj); - Add(modification); - } - - public Asn1SequenceOf Modifications => (Asn1SequenceOf)Get(1); - - public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.ModifyRequest); - - public string GetRequestDN() => ((Asn1OctetString)Get(0)).StringValue(); - } - - internal class RfcModifyResponse : RfcLdapResult - { - public RfcModifyResponse(Stream stream, int len) - : base(stream, len) - { - } - - public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.ModifyResponse); - } +using System; +using System.IO; + +namespace Unosquare.Swan.Networking.Ldap { + /// + /// Represents an Ldap Modify Request. + ///
    +  /// ModifyRequest ::= [APPLICATION 6] SEQUENCE {
    +  /// object          LdapDN,
    +  /// modification    SEQUENCE OF SEQUENCE {
    +  /// operation       ENUMERATED {
    +  /// add     (0),
    +  /// delete  (1),
    +  /// replace (2) },
    +  /// modification    AttributeTypeAndValues } }
    +  /// 
    + ///
    + /// + /// + internal sealed class RfcModifyRequest + : Asn1Sequence, IRfcRequest { + public RfcModifyRequest(String obj, Asn1SequenceOf modification) + : base(2) { + this.Add(obj); + this.Add(modification); + } + + public Asn1SequenceOf Modifications => (Asn1SequenceOf)this.Get(1); + + public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.ModifyRequest); + + public String GetRequestDN() => ((Asn1OctetString)this.Get(0)).StringValue(); + } + + internal class RfcModifyResponse : RfcLdapResult { + public RfcModifyResponse(Stream stream, Int32 len) + : base(stream, len) { + } + + public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.ModifyResponse); + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/SmtpClient.cs b/Unosquare.Swan/Networking/SmtpClient.cs index 592c614..b0e3b21 100644 --- a/Unosquare.Swan/Networking/SmtpClient.cs +++ b/Unosquare.Swan/Networking/SmtpClient.cs @@ -1,397 +1,390 @@ -namespace Unosquare.Swan.Networking -{ - using System.Threading; - using System; - using System.Linq; - using System.Net; - using System.Net.Sockets; - using System.Security; - using System.Text; - using System.Net.Security; - using System.Threading.Tasks; - using System.Collections.Generic; +using System.Threading; +using System; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Security; +using System.Text; +using System.Net.Security; +using System.Threading.Tasks; +using System.Collections.Generic; #if !NETSTANDARD1_3 - using System.Net.Mail; +using System.Net.Mail; #else - using Exceptions; +using Exceptions; #endif +namespace Unosquare.Swan.Networking { + /// + /// Represents a basic SMTP client that is capable of submitting messages to an SMTP server. + /// + /// + /// The following code explains how to send a simple e-mail. + /// + /// using System.Net.Mail; + /// + /// class Example + /// { + /// static void Main() + /// { + /// // create a new smtp client using google's smtp server + /// var client = new SmtpClient("smtp.gmail.com", 587); + /// + /// // send an email + /// client.SendMailAsync( + /// new MailMessage("sender@test.com", "recipient@test.cm", "Subject", "Body")); + /// } + /// } + /// + /// + /// The following code demonstrates how to sent an e-mail using a SmtpSessionState: + /// + /// class Example + /// { + /// static void Main() + /// { + /// // create a new smtp client using google's smtp server + /// var client = new SmtpClient("smtp.gmail.com", 587); + /// + /// // create a new session state with a sender address + /// var session = new SmtpSessionState { SenderAddress = "sender@test.com" }; + /// + /// // add a recipient + /// session.Recipients.Add("recipient@test.cm"); + /// + /// // send + /// client.SendMailAsync(session); + /// } + /// } + /// + /// + /// The following code shows how to send an e-mail with an attachment: + /// + /// using System.Net.Mail; + /// + /// class Example + /// { + /// static void Main() + /// { + /// // create a new smtp client using google's smtp server + /// var client = new SmtpClient("smtp.gmail.com", 587); + /// + /// // create a new session state with a sender address + /// var session = new SmtpSessionState { SenderAddress = "sender@test.com" }; + /// + /// // add a recipient + /// session.Recipients.Add("recipient@test.cm"); + /// + /// // load a file as an attachment + /// var attachment = new MimePart("image", "gif") + /// { + /// Content = new + /// MimeContent(File.OpenRead("meme.gif"), ContentEncoding.Default), + /// ContentDisposition = + /// new ContentDisposition(ContentDisposition.Attachment), + /// ContentTransferEncoding = ContentEncoding.Base64, + /// FileName = Path.GetFileName("meme.gif") + /// }; + /// + /// // send + /// client.SendMailAsync(session); + /// } + /// } + /// + /// + public class SmtpClient { /// - /// Represents a basic SMTP client that is capable of submitting messages to an SMTP server. + /// Initializes a new instance of the class. /// - /// - /// The following code explains how to send a simple e-mail. - /// - /// using System.Net.Mail; - /// - /// class Example - /// { - /// static void Main() - /// { - /// // create a new smtp client using google's smtp server - /// var client = new SmtpClient("smtp.gmail.com", 587); - /// - /// // send an email - /// client.SendMailAsync( - /// new MailMessage("sender@test.com", "recipient@test.cm", "Subject", "Body")); - /// } - /// } - /// - /// - /// The following code demonstrates how to sent an e-mail using a SmtpSessionState: - /// - /// class Example - /// { - /// static void Main() - /// { - /// // create a new smtp client using google's smtp server - /// var client = new SmtpClient("smtp.gmail.com", 587); - /// - /// // create a new session state with a sender address - /// var session = new SmtpSessionState { SenderAddress = "sender@test.com" }; - /// - /// // add a recipient - /// session.Recipients.Add("recipient@test.cm"); - /// - /// // send - /// client.SendMailAsync(session); - /// } - /// } - /// - /// - /// The following code shows how to send an e-mail with an attachment: - /// - /// using System.Net.Mail; - /// - /// class Example - /// { - /// static void Main() - /// { - /// // create a new smtp client using google's smtp server - /// var client = new SmtpClient("smtp.gmail.com", 587); - /// - /// // create a new session state with a sender address - /// var session = new SmtpSessionState { SenderAddress = "sender@test.com" }; - /// - /// // add a recipient - /// session.Recipients.Add("recipient@test.cm"); - /// - /// // load a file as an attachment - /// var attachment = new MimePart("image", "gif") - /// { - /// Content = new - /// MimeContent(File.OpenRead("meme.gif"), ContentEncoding.Default), - /// ContentDisposition = - /// new ContentDisposition(ContentDisposition.Attachment), - /// ContentTransferEncoding = ContentEncoding.Base64, - /// FileName = Path.GetFileName("meme.gif") - /// }; - /// - /// // send - /// client.SendMailAsync(session); - /// } - /// } - /// - /// - public class SmtpClient - { - /// - /// Initializes a new instance of the class. - /// - /// The host. - /// The port. - /// host. - public SmtpClient(string host, int port) - { - Host = host ?? throw new ArgumentNullException(nameof(host)); - Port = port; - ClientHostname = Network.HostName; - } - - /// - /// Gets or sets the credentials. No credentials will be used if set to null. - /// - /// - /// The credentials. - /// - public NetworkCredential Credentials { get; set; } - - /// - /// Gets the host. - /// - /// - /// The host. - /// - public string Host { get; } - - /// - /// Gets the port. - /// - /// - /// The port. - /// - public int Port { get; } - - /// - /// Gets or sets a value indicating whether the SSL is enabled. - /// If set to false, communication between client and server will not be secured. - /// - /// - /// true if [enable SSL]; otherwise, false. - /// - public bool EnableSsl { get; set; } - - /// - /// Gets or sets the name of the client that gets announced to the server. - /// - /// - /// The client hostname. - /// - public string ClientHostname { get; set; } - + /// The host. + /// The port. + /// host. + public SmtpClient(String host, Int32 port) { + this.Host = host ?? throw new ArgumentNullException(nameof(host)); + this.Port = port; + this.ClientHostname = Network.HostName; + } + + /// + /// Gets or sets the credentials. No credentials will be used if set to null. + /// + /// + /// The credentials. + /// + public NetworkCredential Credentials { + get; set; + } + + /// + /// Gets the host. + /// + /// + /// The host. + /// + public String Host { + get; + } + + /// + /// Gets the port. + /// + /// + /// The port. + /// + public Int32 Port { + get; + } + + /// + /// Gets or sets a value indicating whether the SSL is enabled. + /// If set to false, communication between client and server will not be secured. + /// + /// + /// true if [enable SSL]; otherwise, false. + /// + public Boolean EnableSsl { + get; set; + } + + /// + /// Gets or sets the name of the client that gets announced to the server. + /// + /// + /// The client hostname. + /// + public String ClientHostname { + get; set; + } + #if !NETSTANDARD1_3 - /// - /// Sends an email message asynchronously. - /// - /// The message. - /// The session identifier. - /// The cancellation token. - /// The callback. - /// - /// A task that represents the asynchronous of send email operation. - /// - /// message. - public Task SendMailAsync( - MailMessage message, - string sessionId = null, - CancellationToken ct = default, - RemoteCertificateValidationCallback callback = null) - { - if (message == null) - throw new ArgumentNullException(nameof(message)); - - var state = new SmtpSessionState - { - AuthMode = Credentials == null ? string.Empty : SmtpDefinitions.SmtpAuthMethods.Login, - ClientHostname = ClientHostname, - IsChannelSecure = EnableSsl, - SenderAddress = message.From.Address, - }; - - if (Credentials != null) - { - state.Username = Credentials.UserName; - state.Password = Credentials.Password; - } - - foreach (var recipient in message.To) - { - state.Recipients.Add(recipient.Address); - } - - state.DataBuffer.AddRange(message.ToMimeMessage().ToArray()); - - return SendMailAsync(state, sessionId, ct, callback); - } + /// + /// Sends an email message asynchronously. + /// + /// The message. + /// The session identifier. + /// The cancellation token. + /// The callback. + /// + /// A task that represents the asynchronous of send email operation. + /// + /// message. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0067:Objekte verwerfen, bevor Bereich verloren geht", Justification = "")] + public Task SendMailAsync( + MailMessage message, + String sessionId = null, + CancellationToken ct = default, + RemoteCertificateValidationCallback callback = null) { + if(message == null) { + throw new ArgumentNullException(nameof(message)); + } + + SmtpSessionState state = new SmtpSessionState { + AuthMode = this.Credentials == null ? String.Empty : SmtpDefinitions.SmtpAuthMethods.Login, + ClientHostname = ClientHostname, + IsChannelSecure = EnableSsl, + SenderAddress = message.From.Address, + }; + + if(this.Credentials != null) { + state.Username = this.Credentials.UserName; + state.Password = this.Credentials.Password; + } + + foreach(MailAddress recipient in message.To) { + state.Recipients.Add(recipient.Address); + } + + state.DataBuffer.AddRange(message.ToMimeMessage().ToArray()); + + return this.SendMailAsync(state, sessionId, ct, callback); + } #endif - - /// - /// Sends an email message using a session state object. - /// Credentials, Enable SSL and Client Hostname are NOT taken from the state object but - /// rather from the properties of this class. - /// - /// The state. - /// The session identifier. - /// The cancellation token. - /// The callback. - /// - /// A task that represents the asynchronous of send email operation. - /// - /// sessionState. - public Task SendMailAsync( - SmtpSessionState sessionState, - string sessionId = null, - CancellationToken ct = default, - RemoteCertificateValidationCallback callback = null) - { - if (sessionState == null) - throw new ArgumentNullException(nameof(sessionState)); - - return SendMailAsync(new[] { sessionState }, sessionId, ct, callback); - } - - /// - /// Sends an array of email messages using a session state object. - /// Credentials, Enable SSL and Client Hostname are NOT taken from the state object but - /// rather from the properties of this class. - /// - /// The session states. - /// The session identifier. - /// The cancellation token. - /// The callback. - /// - /// A task that represents the asynchronous of send email operation. - /// - /// sessionStates. - /// Could not upgrade the channel to SSL. - /// Defines an SMTP Exceptions class. - public async Task SendMailAsync( - IEnumerable sessionStates, - string sessionId = null, - CancellationToken ct = default, - RemoteCertificateValidationCallback callback = null) - { - if (sessionStates == null) - throw new ArgumentNullException(nameof(sessionStates)); - - using (var tcpClient = new TcpClient()) - { - await tcpClient.ConnectAsync(Host, Port).ConfigureAwait(false); - - using (var connection = new Connection(tcpClient, Encoding.UTF8, "\r\n", true, 1000)) - { - var sender = new SmtpSender(sessionId); - - try - { - // Read the greeting message - sender.ReplyText = await connection.ReadLineAsync(ct).ConfigureAwait(false); - - // EHLO 1 - await SendEhlo(ct, sender, connection).ConfigureAwait(false); - - // STARTTLS - if (EnableSsl) - { - sender.RequestText = $"{SmtpCommandNames.STARTTLS}"; - - await connection.WriteLineAsync(sender.RequestText, ct).ConfigureAwait(false); - sender.ReplyText = await connection.ReadLineAsync(ct).ConfigureAwait(false); - sender.ValidateReply(); - - if (await connection.UpgradeToSecureAsClientAsync(callback: callback).ConfigureAwait(false) == false) - throw new SecurityException("Could not upgrade the channel to SSL."); - } - - // EHLO 2 - await SendEhlo(ct, sender, connection).ConfigureAwait(false); - - // AUTH - if (Credentials != null) - { - var auth = new ConnectionAuth(connection, sender, Credentials); - await auth.AuthenticateAsync(ct).ConfigureAwait(false); - } - - foreach (var sessionState in sessionStates) - { - { - // MAIL FROM - sender.RequestText = $"{SmtpCommandNames.MAIL} FROM:<{sessionState.SenderAddress}>"; - - await connection.WriteLineAsync(sender.RequestText, ct).ConfigureAwait(false); - sender.ReplyText = await connection.ReadLineAsync(ct).ConfigureAwait(false); - sender.ValidateReply(); - } - - // RCPT TO - foreach (var recipient in sessionState.Recipients) - { - sender.RequestText = $"{SmtpCommandNames.RCPT} TO:<{recipient}>"; - - await connection.WriteLineAsync(sender.RequestText, ct).ConfigureAwait(false); - sender.ReplyText = await connection.ReadLineAsync(ct).ConfigureAwait(false); - sender.ValidateReply(); - } - - { - // DATA - sender.RequestText = $"{SmtpCommandNames.DATA}"; - - await connection.WriteLineAsync(sender.RequestText, ct).ConfigureAwait(false); - sender.ReplyText = await connection.ReadLineAsync(ct).ConfigureAwait(false); - sender.ValidateReply(); - } - - { - // CONTENT - var dataTerminator = sessionState.DataBuffer - .Skip(sessionState.DataBuffer.Count - 5) - .ToText(); - - sender.RequestText = $"Buffer ({sessionState.DataBuffer.Count} bytes)"; - - await connection.WriteDataAsync(sessionState.DataBuffer.ToArray(), true, ct).ConfigureAwait(false); - if (dataTerminator.EndsWith(SmtpDefinitions.SmtpDataCommandTerminator) == false) - await connection.WriteTextAsync(SmtpDefinitions.SmtpDataCommandTerminator, ct).ConfigureAwait(false); - - sender.ReplyText = await connection.ReadLineAsync(ct).ConfigureAwait(false); - sender.ValidateReply(); - } - } - - { - // QUIT - sender.RequestText = $"{SmtpCommandNames.QUIT}"; - - await connection.WriteLineAsync(sender.RequestText, ct).ConfigureAwait(false); - sender.ReplyText = await connection.ReadLineAsync(ct).ConfigureAwait(false); - sender.ValidateReply(); - } - } - catch (Exception ex) - { - var errorMessage = - $"Could not send email. {ex.Message}\r\n Last Request: {sender.RequestText}\r\n Last Reply: {sender.ReplyText}"; - errorMessage.Error(typeof(SmtpClient).FullName, sessionId); - - throw new SmtpException(errorMessage); - } - } - } - } - - private async Task SendEhlo(CancellationToken ct, SmtpSender sender, Connection connection) - { - sender.RequestText = $"{SmtpCommandNames.EHLO} {ClientHostname}"; - - await connection.WriteLineAsync(sender.RequestText, ct).ConfigureAwait(false); - - do - { - sender.ReplyText = await connection.ReadLineAsync(ct).ConfigureAwait(false); - } while (!sender.IsReplyOk); - - sender.ValidateReply(); - } - - private class ConnectionAuth - { - private readonly SmtpSender _sender; - private readonly Connection _connection; - private readonly NetworkCredential _credentials; - - public ConnectionAuth(Connection connection, SmtpSender sender, NetworkCredential credentials) - { - _connection = connection; - _sender = sender; - _credentials = credentials; - } - - public async Task AuthenticateAsync(CancellationToken ct) - { - _sender.RequestText = - $"{SmtpCommandNames.AUTH} {SmtpDefinitions.SmtpAuthMethods.Login} {Convert.ToBase64String(Encoding.UTF8.GetBytes(_credentials.UserName))}"; - - await _connection.WriteLineAsync(_sender.RequestText, ct).ConfigureAwait(false); - _sender.ReplyText = await _connection.ReadLineAsync(ct).ConfigureAwait(false); - _sender.ValidateReply(); - _sender.RequestText = Convert.ToBase64String(Encoding.UTF8.GetBytes(_credentials.Password)); - - await _connection.WriteLineAsync(_sender.RequestText, ct).ConfigureAwait(false); - _sender.ReplyText = await _connection.ReadLineAsync(ct).ConfigureAwait(false); - _sender.ValidateReply(); - } - } - } + + /// + /// Sends an email message using a session state object. + /// Credentials, Enable SSL and Client Hostname are NOT taken from the state object but + /// rather from the properties of this class. + /// + /// The state. + /// The session identifier. + /// The cancellation token. + /// The callback. + /// + /// A task that represents the asynchronous of send email operation. + /// + /// sessionState. + public Task SendMailAsync( + SmtpSessionState sessionState, + String sessionId = null, + CancellationToken ct = default, + RemoteCertificateValidationCallback callback = null) { + if(sessionState == null) { + throw new ArgumentNullException(nameof(sessionState)); + } + + return this.SendMailAsync(new[] { sessionState }, sessionId, ct, callback); + } + + /// + /// Sends an array of email messages using a session state object. + /// Credentials, Enable SSL and Client Hostname are NOT taken from the state object but + /// rather from the properties of this class. + /// + /// The session states. + /// The session identifier. + /// The cancellation token. + /// The callback. + /// + /// A task that represents the asynchronous of send email operation. + /// + /// sessionStates. + /// Could not upgrade the channel to SSL. + /// Defines an SMTP Exceptions class. + public async Task SendMailAsync( + IEnumerable sessionStates, + String sessionId = null, + CancellationToken ct = default, + RemoteCertificateValidationCallback callback = null) { + if(sessionStates == null) { + throw new ArgumentNullException(nameof(sessionStates)); + } + + using(TcpClient tcpClient = new TcpClient()) { + await tcpClient.ConnectAsync(this.Host, this.Port).ConfigureAwait(false); + + using(Connection connection = new Connection(tcpClient, Encoding.UTF8, "\r\n", true, 1000)) { + SmtpSender sender = new SmtpSender(sessionId); + + try { + // Read the greeting message + sender.ReplyText = await connection.ReadLineAsync(ct).ConfigureAwait(false); + + // EHLO 1 + await this.SendEhlo(ct, sender, connection).ConfigureAwait(false); + + // STARTTLS + if(this.EnableSsl) { + sender.RequestText = $"{SmtpCommandNames.STARTTLS}"; + + await connection.WriteLineAsync(sender.RequestText, ct).ConfigureAwait(false); + sender.ReplyText = await connection.ReadLineAsync(ct).ConfigureAwait(false); + sender.ValidateReply(); + + if(await connection.UpgradeToSecureAsClientAsync(callback: callback).ConfigureAwait(false) == false) { + throw new SecurityException("Could not upgrade the channel to SSL."); + } + } + + // EHLO 2 + await this.SendEhlo(ct, sender, connection).ConfigureAwait(false); + + // AUTH + if(this.Credentials != null) { + ConnectionAuth auth = new ConnectionAuth(connection, sender, this.Credentials); + await auth.AuthenticateAsync(ct).ConfigureAwait(false); + } + + foreach(SmtpSessionState sessionState in sessionStates) { + { + // MAIL FROM + sender.RequestText = $"{SmtpCommandNames.MAIL} FROM:<{sessionState.SenderAddress}>"; + + await connection.WriteLineAsync(sender.RequestText, ct).ConfigureAwait(false); + sender.ReplyText = await connection.ReadLineAsync(ct).ConfigureAwait(false); + sender.ValidateReply(); + } + + // RCPT TO + foreach(String recipient in sessionState.Recipients) { + sender.RequestText = $"{SmtpCommandNames.RCPT} TO:<{recipient}>"; + + await connection.WriteLineAsync(sender.RequestText, ct).ConfigureAwait(false); + sender.ReplyText = await connection.ReadLineAsync(ct).ConfigureAwait(false); + sender.ValidateReply(); + } + + { + // DATA + sender.RequestText = $"{SmtpCommandNames.DATA}"; + + await connection.WriteLineAsync(sender.RequestText, ct).ConfigureAwait(false); + sender.ReplyText = await connection.ReadLineAsync(ct).ConfigureAwait(false); + sender.ValidateReply(); + } + + { + // CONTENT + String dataTerminator = sessionState.DataBuffer + .Skip(sessionState.DataBuffer.Count - 5) + .ToText(); + + sender.RequestText = $"Buffer ({sessionState.DataBuffer.Count} bytes)"; + + await connection.WriteDataAsync(sessionState.DataBuffer.ToArray(), true, ct).ConfigureAwait(false); + if(dataTerminator.EndsWith(SmtpDefinitions.SmtpDataCommandTerminator) == false) { + await connection.WriteTextAsync(SmtpDefinitions.SmtpDataCommandTerminator, ct).ConfigureAwait(false); + } + + sender.ReplyText = await connection.ReadLineAsync(ct).ConfigureAwait(false); + sender.ValidateReply(); + } + } + + { + // QUIT + sender.RequestText = $"{SmtpCommandNames.QUIT}"; + + await connection.WriteLineAsync(sender.RequestText, ct).ConfigureAwait(false); + sender.ReplyText = await connection.ReadLineAsync(ct).ConfigureAwait(false); + sender.ValidateReply(); + } + } catch(Exception ex) { + String errorMessage = + $"Could not send email. {ex.Message}\r\n Last Request: {sender.RequestText}\r\n Last Reply: {sender.ReplyText}"; + errorMessage.Error(typeof(SmtpClient).FullName, sessionId); + + throw new SmtpException(errorMessage); + } + } + } + } + + private async Task SendEhlo(CancellationToken ct, SmtpSender sender, Connection connection) { + sender.RequestText = $"{SmtpCommandNames.EHLO} {this.ClientHostname}"; + + await connection.WriteLineAsync(sender.RequestText, ct).ConfigureAwait(false); + + do { + sender.ReplyText = await connection.ReadLineAsync(ct).ConfigureAwait(false); + } while(!sender.IsReplyOk); + + sender.ValidateReply(); + } + + private class ConnectionAuth { + private readonly SmtpSender _sender; + private readonly Connection _connection; + private readonly NetworkCredential _credentials; + + public ConnectionAuth(Connection connection, SmtpSender sender, NetworkCredential credentials) { + this._connection = connection; + this._sender = sender; + this._credentials = credentials; + } + + public async Task AuthenticateAsync(CancellationToken ct) { + this._sender.RequestText = + $"{SmtpCommandNames.AUTH} {SmtpDefinitions.SmtpAuthMethods.Login} {Convert.ToBase64String(Encoding.UTF8.GetBytes(this._credentials.UserName))}"; + + await this._connection.WriteLineAsync(this._sender.RequestText, ct).ConfigureAwait(false); + this._sender.ReplyText = await this._connection.ReadLineAsync(ct).ConfigureAwait(false); + this._sender.ValidateReply(); + this._sender.RequestText = Convert.ToBase64String(Encoding.UTF8.GetBytes(this._credentials.Password)); + + await this._connection.WriteLineAsync(this._sender.RequestText, ct).ConfigureAwait(false); + this._sender.ReplyText = await this._connection.ReadLineAsync(ct).ConfigureAwait(false); + this._sender.ValidateReply(); + } + } + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/SmtpDefinitions.cs b/Unosquare.Swan/Networking/SmtpDefinitions.cs index dff0951..5b02d28 100644 --- a/Unosquare.Swan/Networking/SmtpDefinitions.cs +++ b/Unosquare.Swan/Networking/SmtpDefinitions.cs @@ -1,29 +1,28 @@ -namespace Unosquare.Swan.Networking -{ +using System; + +namespace Unosquare.Swan.Networking { + /// + /// Contains useful constants and definitions. + /// + public static class SmtpDefinitions { /// - /// Contains useful constants and definitions. + /// The string sequence that delimits the end of the DATA command. /// - public static class SmtpDefinitions - { - /// - /// The string sequence that delimits the end of the DATA command. - /// - public const string SmtpDataCommandTerminator = "\r\n.\r\n"; - - /// - /// Lists the AUTH methods supported by default. - /// - public static class SmtpAuthMethods - { - /// - /// The plain method. - /// - public const string Plain = "PLAIN"; - - /// - /// The login method. - /// - public const string Login = "LOGIN"; - } - } + public const String SmtpDataCommandTerminator = "\r\n.\r\n"; + + /// + /// Lists the AUTH methods supported by default. + /// + public static class SmtpAuthMethods { + /// + /// The plain method. + /// + public const String Plain = "PLAIN"; + + /// + /// The login method. + /// + public const String Login = "LOGIN"; + } + } } diff --git a/Unosquare.Swan/Networking/SmtpSender.cs b/Unosquare.Swan/Networking/SmtpSender.cs index 0273081..2a19f7c 100644 --- a/Unosquare.Swan/Networking/SmtpSender.cs +++ b/Unosquare.Swan/Networking/SmtpSender.cs @@ -1,60 +1,55 @@ -namespace Unosquare.Swan.Networking -{ +using System; #if !NETSTANDARD1_3 - using System.Net.Mail; +using System.Net.Mail; #else - using Exceptions; +using Exceptions; #endif - - /// - /// Use this class to store the sender session data. - /// - internal class SmtpSender - { - private readonly string _sessionId; - private string _requestText; - - public SmtpSender(string sessionId) - { - _sessionId = sessionId; - } - - public string RequestText - { - get => _requestText; - set - { - _requestText = value; - $" TX {_requestText}".Debug(typeof(SmtpClient), _sessionId); - } - } - - public string ReplyText { get; set; } - - public bool IsReplyOk => ReplyText.StartsWith("250 "); - - public void ValidateReply() - { - if (ReplyText == null) - throw new SmtpException("There was no response from the server"); - - try - { - var response = SmtpServerReply.Parse(ReplyText); - $" RX {ReplyText} - {response.IsPositive}".Debug(typeof(SmtpClient), _sessionId); - - if (response.IsPositive) return; - - var responseContent = string.Empty; - if (response.Content.Count > 0) - responseContent = string.Join(";", response.Content.ToArray()); - - throw new SmtpException((SmtpStatusCode)response.ReplyCode, responseContent); - } - catch - { - throw new SmtpException($"Could not parse server response: {ReplyText}"); - } - } - } +namespace Unosquare.Swan.Networking { + /// + /// Use this class to store the sender session data. + /// + internal class SmtpSender { + private readonly String _sessionId; + private String _requestText; + + public SmtpSender(String sessionId) => this._sessionId = sessionId; + + public String RequestText { + get => this._requestText; + set { + this._requestText = value; + $" TX {this._requestText}".Debug(typeof(SmtpClient), this._sessionId); + } + } + + public String ReplyText { + get; set; + } + + public Boolean IsReplyOk => this.ReplyText.StartsWith("250 "); + + public void ValidateReply() { + if(this.ReplyText == null) { + throw new SmtpException("There was no response from the server"); + } + + try { + SmtpServerReply response = SmtpServerReply.Parse(this.ReplyText); + $" RX {this.ReplyText} - {response.IsPositive}".Debug(typeof(SmtpClient), this._sessionId); + + if(response.IsPositive) { + return; + } + + String responseContent = String.Empty; + if(response.Content.Count > 0) { + responseContent = String.Join(";", response.Content.ToArray()); + } + + throw new SmtpException((SmtpStatusCode)response.ReplyCode, responseContent); + } catch { + throw new SmtpException($"Could not parse server response: {this.ReplyText}"); + } + } + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/SmtpServerReply.cs b/Unosquare.Swan/Networking/SmtpServerReply.cs index d62b3b5..7553329 100644 --- a/Unosquare.Swan/Networking/SmtpServerReply.cs +++ b/Unosquare.Swan/Networking/SmtpServerReply.cs @@ -1,243 +1,267 @@ -namespace Unosquare.Swan.Networking -{ - using System; - using System.Collections.Generic; - using System.Globalization; - using System.Linq; - using System.Text; - +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace Unosquare.Swan.Networking { + /// + /// Represents an SMTP server response object. + /// + public class SmtpServerReply { + #region Constructors + /// - /// Represents an SMTP server response object. + /// Initializes a new instance of the class. /// - public class SmtpServerReply - { - #region Constructors - - /// - /// Initializes a new instance of the class. - /// - /// The response code. - /// The status code. - /// The content. - public SmtpServerReply(int responseCode, string statusCode, params string[] content) - { - Content = new List(); - ReplyCode = responseCode; - EnhancedStatusCode = statusCode; - Content.AddRange(content); - IsValid = responseCode >= 200 && responseCode < 600; - ReplyCodeSeverity = SmtpReplyCodeSeverities.Unknown; - ReplyCodeCategory = SmtpReplyCodeCategories.Unknown; - - if (!IsValid) return; - if (responseCode >= 200) ReplyCodeSeverity = SmtpReplyCodeSeverities.PositiveCompletion; - if (responseCode >= 300) ReplyCodeSeverity = SmtpReplyCodeSeverities.PositiveIntermediate; - if (responseCode >= 400) ReplyCodeSeverity = SmtpReplyCodeSeverities.TransientNegative; - if (responseCode >= 500) ReplyCodeSeverity = SmtpReplyCodeSeverities.PermanentNegative; - if (responseCode >= 600) ReplyCodeSeverity = SmtpReplyCodeSeverities.Unknown; - - if (int.TryParse(responseCode.ToString(CultureInfo.InvariantCulture).Substring(1, 1), out var middleDigit)) - { - if (middleDigit >= 0 && middleDigit <= 5) - ReplyCodeCategory = (SmtpReplyCodeCategories) middleDigit; - } - } - - /// - /// Initializes a new instance of the class. - /// - public SmtpServerReply() - : this(0, string.Empty, string.Empty) - { - // placeholder - } - - /// - /// Initializes a new instance of the class. - /// - /// The response code. - /// The status code. - /// The content. - public SmtpServerReply(int responseCode, string statusCode, string content) - : this(responseCode, statusCode, new[] {content}) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The response code. - /// The content. - public SmtpServerReply(int responseCode, string content) - : this(responseCode, string.Empty, content) - { - } - - #endregion - - #region Pre-built responses (https://tools.ietf.org/html/rfc5321#section-4.2.2) - - /// - /// Gets the command unrecognized reply. - /// - public static SmtpServerReply CommandUnrecognized => - new SmtpServerReply(500, "Syntax error, command unrecognized"); - - /// - /// Gets the syntax error arguments reply. - /// - public static SmtpServerReply SyntaxErrorArguments => - new SmtpServerReply(501, "Syntax error in parameters or arguments"); - - /// - /// Gets the command not implemented reply. - /// - public static SmtpServerReply CommandNotImplemented => new SmtpServerReply(502, "Command not implemented"); - - /// - /// Gets the bad sequence of commands reply. - /// - public static SmtpServerReply BadSequenceOfCommands => new SmtpServerReply(503, "Bad sequence of commands"); - - /// - /// Gets the protocol violation reply. - /// = - public static SmtpServerReply ProtocolViolation => - new SmtpServerReply(451, "Requested action aborted: error in processing"); - - /// - /// Gets the system status bye reply. - /// - public static SmtpServerReply SystemStatusBye => - new SmtpServerReply(221, "Service closing transmission channel"); - - /// - /// Gets the system status help reply. - /// = - public static SmtpServerReply SystemStatusHelp => new SmtpServerReply(221, "Refer to RFC 5321"); - - /// - /// Gets the bad syntax command empty reply. - /// - public static SmtpServerReply BadSyntaxCommandEmpty => new SmtpServerReply(400, "Error: bad syntax"); - - /// - /// Gets the OK reply. - /// - public static SmtpServerReply Ok => new SmtpServerReply(250, "OK"); - - /// - /// Gets the authorization required reply. - /// - public static SmtpServerReply AuthorizationRequired => new SmtpServerReply(530, "Authorization Required"); - - #endregion - - #region Properties - - /// - /// Gets the response severity. - /// - public SmtpReplyCodeSeverities ReplyCodeSeverity { get; } - - /// - /// Gets the response category. - /// - public SmtpReplyCodeCategories ReplyCodeCategory { get; } - - /// - /// Gets the numeric response code. - /// - public int ReplyCode { get; } - - /// - /// Gets the enhanced status code. - /// - public string EnhancedStatusCode { get; } - - /// - /// Gets the content. - /// - public List Content { get; } - - /// - /// Returns true if the response code is between 200 and 599. - /// - public bool IsValid { get; } - - /// - /// Gets a value indicating whether this instance is positive. - /// - public bool IsPositive => ReplyCode >= 200 && ReplyCode <= 399; - - #endregion - - #region Methods - - /// - /// Parses the specified text into a Server Reply for thorough analysis. - /// - /// The text. - /// A new instance of SMTP server response object. - public static SmtpServerReply Parse(string text) - { - var lines = text.Split(new[] {"\r\n"}, StringSplitOptions.RemoveEmptyEntries); - if (lines.Length == 0) return new SmtpServerReply(); - - var lastLineParts = lines.Last().Split(new[] {" "}, StringSplitOptions.RemoveEmptyEntries); - var enhancedStatusCode = string.Empty; - int.TryParse(lastLineParts[0], out var responseCode); - if (lastLineParts.Length > 1) - { - if (lastLineParts[1].Split('.').Length == 3) - enhancedStatusCode = lastLineParts[1]; - } - - var content = new List(); - - for (var i = 0; i < lines.Length; i++) - { - var splitChar = i == lines.Length - 1 ? " " : "-"; - - var lineParts = lines[i].Split(new[] {splitChar}, 2, StringSplitOptions.None); - var lineContent = lineParts.Last(); - if (string.IsNullOrWhiteSpace(enhancedStatusCode) == false) - lineContent = lineContent.Replace(enhancedStatusCode, string.Empty).Trim(); - - content.Add(lineContent); - } - - return new SmtpServerReply(responseCode, enhancedStatusCode, content.ToArray()); - } - - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public override string ToString() - { - var responseCodeText = ReplyCode.ToString(CultureInfo.InvariantCulture); - var statusCodeText = string.IsNullOrWhiteSpace(EnhancedStatusCode) - ? string.Empty - : $" {EnhancedStatusCode.Trim()}"; - if (Content.Count == 0) return $"{responseCodeText}{statusCodeText}"; - - var builder = new StringBuilder(); - - for (var i = 0; i < Content.Count; i++) - { - var isLastLine = i == Content.Count - 1; - - builder.Append(isLastLine - ? $"{responseCodeText}{statusCodeText} {Content[i]}" - : $"{responseCodeText}-{Content[i]}\r\n"); - } - - return builder.ToString(); - } - - #endregion - } + /// The response code. + /// The status code. + /// The content. + public SmtpServerReply(Int32 responseCode, String statusCode, params String[] content) { + this.Content = new List(); + this.ReplyCode = responseCode; + this.EnhancedStatusCode = statusCode; + this.Content.AddRange(content); + this.IsValid = responseCode >= 200 && responseCode < 600; + this.ReplyCodeSeverity = SmtpReplyCodeSeverities.Unknown; + this.ReplyCodeCategory = SmtpReplyCodeCategories.Unknown; + + if(!this.IsValid) { + return; + } + + if(responseCode >= 200) { + this.ReplyCodeSeverity = SmtpReplyCodeSeverities.PositiveCompletion; + } + + if(responseCode >= 300) { + this.ReplyCodeSeverity = SmtpReplyCodeSeverities.PositiveIntermediate; + } + + if(responseCode >= 400) { + this.ReplyCodeSeverity = SmtpReplyCodeSeverities.TransientNegative; + } + + if(responseCode >= 500) { + this.ReplyCodeSeverity = SmtpReplyCodeSeverities.PermanentNegative; + } + + if(responseCode >= 600) { + this.ReplyCodeSeverity = SmtpReplyCodeSeverities.Unknown; + } + + if(Int32.TryParse(responseCode.ToString(CultureInfo.InvariantCulture).Substring(1, 1), out Int32 middleDigit)) { + if(middleDigit >= 0 && middleDigit <= 5) { + this.ReplyCodeCategory = (SmtpReplyCodeCategories)middleDigit; + } + } + } + + /// + /// Initializes a new instance of the class. + /// + public SmtpServerReply() + : this(0, String.Empty, String.Empty) { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// + /// The response code. + /// The status code. + /// The content. + public SmtpServerReply(Int32 responseCode, String statusCode, String content) + : this(responseCode, statusCode, new[] { content }) { + } + + /// + /// Initializes a new instance of the class. + /// + /// The response code. + /// The content. + public SmtpServerReply(Int32 responseCode, String content) + : this(responseCode, String.Empty, content) { + } + + #endregion + + #region Pre-built responses (https://tools.ietf.org/html/rfc5321#section-4.2.2) + + /// + /// Gets the command unrecognized reply. + /// + public static SmtpServerReply CommandUnrecognized => + new SmtpServerReply(500, "Syntax error, command unrecognized"); + + /// + /// Gets the syntax error arguments reply. + /// + public static SmtpServerReply SyntaxErrorArguments => + new SmtpServerReply(501, "Syntax error in parameters or arguments"); + + /// + /// Gets the command not implemented reply. + /// + public static SmtpServerReply CommandNotImplemented => new SmtpServerReply(502, "Command not implemented"); + + /// + /// Gets the bad sequence of commands reply. + /// + public static SmtpServerReply BadSequenceOfCommands => new SmtpServerReply(503, "Bad sequence of commands"); + + /// + /// Gets the protocol violation reply. + /// = + public static SmtpServerReply ProtocolViolation => + new SmtpServerReply(451, "Requested action aborted: error in processing"); + + /// + /// Gets the system status bye reply. + /// + public static SmtpServerReply SystemStatusBye => + new SmtpServerReply(221, "Service closing transmission channel"); + + /// + /// Gets the system status help reply. + /// = + public static SmtpServerReply SystemStatusHelp => new SmtpServerReply(221, "Refer to RFC 5321"); + + /// + /// Gets the bad syntax command empty reply. + /// + public static SmtpServerReply BadSyntaxCommandEmpty => new SmtpServerReply(400, "Error: bad syntax"); + + /// + /// Gets the OK reply. + /// + public static SmtpServerReply Ok => new SmtpServerReply(250, "OK"); + + /// + /// Gets the authorization required reply. + /// + public static SmtpServerReply AuthorizationRequired => new SmtpServerReply(530, "Authorization Required"); + + #endregion + + #region Properties + + /// + /// Gets the response severity. + /// + public SmtpReplyCodeSeverities ReplyCodeSeverity { + get; + } + + /// + /// Gets the response category. + /// + public SmtpReplyCodeCategories ReplyCodeCategory { + get; + } + + /// + /// Gets the numeric response code. + /// + public Int32 ReplyCode { + get; + } + + /// + /// Gets the enhanced status code. + /// + public String EnhancedStatusCode { + get; + } + + /// + /// Gets the content. + /// + public List Content { + get; + } + + /// + /// Returns true if the response code is between 200 and 599. + /// + public Boolean IsValid { + get; + } + + /// + /// Gets a value indicating whether this instance is positive. + /// + public Boolean IsPositive => this.ReplyCode >= 200 && this.ReplyCode <= 399; + + #endregion + + #region Methods + + /// + /// Parses the specified text into a Server Reply for thorough analysis. + /// + /// The text. + /// A new instance of SMTP server response object. + public static SmtpServerReply Parse(String text) { + String[] lines = text.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries); + if(lines.Length == 0) { + return new SmtpServerReply(); + } + + String[] lastLineParts = lines.Last().Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries); + String enhancedStatusCode = String.Empty; + _ = Int32.TryParse(lastLineParts[0], out Int32 responseCode); + if(lastLineParts.Length > 1) { + if(lastLineParts[1].Split('.').Length == 3) { + enhancedStatusCode = lastLineParts[1]; + } + } + + List content = new List(); + + for(Int32 i = 0; i < lines.Length; i++) { + String splitChar = i == lines.Length - 1 ? " " : "-"; + + String[] lineParts = lines[i].Split(new[] { splitChar }, 2, StringSplitOptions.None); + String lineContent = lineParts.Last(); + if(String.IsNullOrWhiteSpace(enhancedStatusCode) == false) { + lineContent = lineContent.Replace(enhancedStatusCode, String.Empty).Trim(); + } + + content.Add(lineContent); + } + + return new SmtpServerReply(responseCode, enhancedStatusCode, content.ToArray()); + } + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override String ToString() { + String responseCodeText = this.ReplyCode.ToString(CultureInfo.InvariantCulture); + String statusCodeText = String.IsNullOrWhiteSpace(this.EnhancedStatusCode) + ? String.Empty + : $" {this.EnhancedStatusCode.Trim()}"; + if(this.Content.Count == 0) { + return $"{responseCodeText}{statusCodeText}"; + } + + StringBuilder builder = new StringBuilder(); + + for(Int32 i = 0; i < this.Content.Count; i++) { + Boolean isLastLine = i == this.Content.Count - 1; + + _ = builder.Append(isLastLine + ? $"{responseCodeText}{statusCodeText} {this.Content[i]}" + : $"{responseCodeText}-{this.Content[i]}\r\n"); + } + + return builder.ToString(); + } + + #endregion + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/SmtpSessionState.cs b/Unosquare.Swan/Networking/SmtpSessionState.cs index 2a506c9..02500c7 100644 --- a/Unosquare.Swan/Networking/SmtpSessionState.cs +++ b/Unosquare.Swan/Networking/SmtpSessionState.cs @@ -1,162 +1,183 @@ -namespace Unosquare.Swan.Networking -{ - using System.Collections.Generic; - +using System; +using System.Collections.Generic; + +namespace Unosquare.Swan.Networking { + /// + /// Represents the state of an SMTP session associated with a client. + /// + public class SmtpSessionState { + #region Constructors + /// - /// Represents the state of an SMTP session associated with a client. + /// Initializes a new instance of the class. /// - public class SmtpSessionState - { - #region Constructors - - /// - /// Initializes a new instance of the class. - /// - public SmtpSessionState() - { - DataBuffer = new List(); - Reset(true); - ResetAuthentication(); - } - - #endregion - - #region Properties - - /// - /// Gets the contents of the data buffer. - /// - public List DataBuffer { get; protected set; } - - /// - /// Gets or sets a value indicating whether this instance has initiated. - /// - public bool HasInitiated { get; set; } - - /// - /// Gets or sets a value indicating whether the current session supports extensions. - /// - public bool SupportsExtensions { get; set; } - - /// - /// Gets or sets the client hostname. - /// - public string ClientHostname { get; set; } - - /// - /// Gets or sets a value indicating whether the session is currently receiving DATA. - /// - public bool IsInDataMode { get; set; } - - /// - /// Gets or sets the sender address. - /// - public string SenderAddress { get; set; } - - /// - /// Gets the recipients. - /// - public List Recipients { get; } = new List(); - - /// - /// Gets or sets the extended data supporting any additional field for storage by a responder implementation. - /// - public object ExtendedData { get; set; } - - #endregion - - #region AUTH State - - /// - /// Gets or sets a value indicating whether this instance is in authentication mode. - /// - public bool IsInAuthMode { get; set; } - - /// - /// Gets or sets the username. - /// - public string Username { get; set; } - - /// - /// Gets or sets the password. - /// - public string Password { get; set; } - - /// - /// Gets a value indicating whether this instance has provided username. - /// - public bool HasProvidedUsername => string.IsNullOrWhiteSpace(Username) == false; - - /// - /// Gets or sets a value indicating whether this instance is authenticated. - /// - public bool IsAuthenticated { get; set; } - - /// - /// Gets or sets the authentication mode. - /// - public string AuthMode { get; set; } - - /// - /// Gets or sets a value indicating whether this instance is channel secure. - /// - public bool IsChannelSecure { get; set; } - - /// - /// Resets the authentication state. - /// - public void ResetAuthentication() - { - Username = string.Empty; - Password = string.Empty; - AuthMode = string.Empty; - IsInAuthMode = false; - IsAuthenticated = false; - } - - #endregion - - #region Methods - - /// - /// Resets the data mode to false, clears the recipients, the sender address and the data buffer. - /// - public void ResetEmail() - { - IsInDataMode = false; - Recipients.Clear(); - SenderAddress = string.Empty; - DataBuffer.Clear(); - } - - /// - /// Resets the state table entirely. - /// - /// if set to true [clear extension data]. - public void Reset(bool clearExtensionData) - { - HasInitiated = false; - SupportsExtensions = false; - ClientHostname = string.Empty; - ResetEmail(); - - if (clearExtensionData) - ExtendedData = null; - } - - /// - /// Creates a new object that is a copy of the current instance. - /// - /// A clone. - public virtual SmtpSessionState Clone() - { - var clonedState = this.CopyPropertiesToNew(new[] {nameof(DataBuffer)}); - clonedState.DataBuffer.AddRange(DataBuffer); - clonedState.Recipients.AddRange(Recipients); - - return clonedState; - } - - #endregion - } + public SmtpSessionState() { + this.DataBuffer = new List(); + this.Reset(true); + this.ResetAuthentication(); + } + + #endregion + + #region Properties + + /// + /// Gets the contents of the data buffer. + /// + public List DataBuffer { + get; protected set; + } + + /// + /// Gets or sets a value indicating whether this instance has initiated. + /// + public Boolean HasInitiated { + get; set; + } + + /// + /// Gets or sets a value indicating whether the current session supports extensions. + /// + public Boolean SupportsExtensions { + get; set; + } + + /// + /// Gets or sets the client hostname. + /// + public String ClientHostname { + get; set; + } + + /// + /// Gets or sets a value indicating whether the session is currently receiving DATA. + /// + public Boolean IsInDataMode { + get; set; + } + + /// + /// Gets or sets the sender address. + /// + public String SenderAddress { + get; set; + } + + /// + /// Gets the recipients. + /// + public List Recipients { get; } = new List(); + + /// + /// Gets or sets the extended data supporting any additional field for storage by a responder implementation. + /// + public Object ExtendedData { + get; set; + } + + #endregion + + #region AUTH State + + /// + /// Gets or sets a value indicating whether this instance is in authentication mode. + /// + public Boolean IsInAuthMode { + get; set; + } + + /// + /// Gets or sets the username. + /// + public String Username { + get; set; + } + + /// + /// Gets or sets the password. + /// + public String Password { + get; set; + } + + /// + /// Gets a value indicating whether this instance has provided username. + /// + public Boolean HasProvidedUsername => String.IsNullOrWhiteSpace(this.Username) == false; + + /// + /// Gets or sets a value indicating whether this instance is authenticated. + /// + public Boolean IsAuthenticated { + get; set; + } + + /// + /// Gets or sets the authentication mode. + /// + public String AuthMode { + get; set; + } + + /// + /// Gets or sets a value indicating whether this instance is channel secure. + /// + public Boolean IsChannelSecure { + get; set; + } + + /// + /// Resets the authentication state. + /// + public void ResetAuthentication() { + this.Username = String.Empty; + this.Password = String.Empty; + this.AuthMode = String.Empty; + this.IsInAuthMode = false; + this.IsAuthenticated = false; + } + + #endregion + + #region Methods + + /// + /// Resets the data mode to false, clears the recipients, the sender address and the data buffer. + /// + public void ResetEmail() { + this.IsInDataMode = false; + this.Recipients.Clear(); + this.SenderAddress = String.Empty; + this.DataBuffer.Clear(); + } + + /// + /// Resets the state table entirely. + /// + /// if set to true [clear extension data]. + public void Reset(Boolean clearExtensionData) { + this.HasInitiated = false; + this.SupportsExtensions = false; + this.ClientHostname = String.Empty; + this.ResetEmail(); + + if(clearExtensionData) { + this.ExtendedData = null; + } + } + + /// + /// Creates a new object that is a copy of the current instance. + /// + /// A clone. + public virtual SmtpSessionState Clone() { + SmtpSessionState clonedState = this.CopyPropertiesToNew(new[] { nameof(this.DataBuffer) }); + clonedState.DataBuffer.AddRange(this.DataBuffer); + clonedState.Recipients.AddRange(this.Recipients); + + return clonedState; + } + + #endregion + } } \ No newline at end of file diff --git a/Unosquare.Swan/Networking/SnmpClient.cs b/Unosquare.Swan/Networking/SnmpClient.cs index 51d6173..25a9825 100644 --- a/Unosquare.Swan/Networking/SnmpClient.cs +++ b/Unosquare.Swan/Networking/SnmpClient.cs @@ -1,37 +1,34 @@ -namespace Unosquare.Swan.Networking -{ - using System; - using System.Collections.Generic; - using System.Net; - using System.Net.Sockets; - using System.Text; - using System.Threading.Tasks; - - /// - /// Represents a little SNMP client based on http://www.java2s.com/Code/CSharp/Network/SimpleSNMP.htm. - /// - public static class SnmpClient +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace Unosquare.Swan.Networking { + /// + /// Represents a little SNMP client based on http://www.java2s.com/Code/CSharp/Network/SimpleSNMP.htm. + /// + public static class SnmpClient { + private static readonly Byte[] DiscoverMessage = { - private static readonly byte[] DiscoverMessage = - { 48, 41, 2, 1, 1, 4, 6, 112, 117, 98, 108, 105, 99, 160, 28, 2, 4, 111, 81, 45, 144, 2, 1, 0, 2, 1, 0, 48, 14, 48, 12, 6, 8, 43, 6, 1, 2, 1, 1, 1, 0, 5, 0, - }; - - /// - /// Discovers the specified SNMP time out. - /// - /// The SNMP time out. - /// An array of network endpoint as an IP address and a port number. - public static IPEndPoint[] Discover(int snmpTimeOut = 6000) - { - var endpoints = new List(); - - Task[] tasks = - { + }; + + /// + /// Discovers the specified SNMP time out. + /// + /// The SNMP time out. + /// An array of network endpoint as an IP address and a port number. + public static IPEndPoint[] Discover(Int32 snmpTimeOut = 6000) { + List endpoints = new List(); + + Task[] tasks = + { Task.Run(async () => { - using (var udp = new UdpClient(IPAddress.Broadcast.AddressFamily)) + using (UdpClient udp = new UdpClient(IPAddress.Broadcast.AddressFamily)) { udp.EnableBroadcast = true; await udp.SendAsync( @@ -43,7 +40,7 @@ { try { - var buffer = new byte[udp.Client.ReceiveBufferSize]; + Byte[] buffer = new Byte[udp.Client.ReceiveBufferSize]; EndPoint remote = new IPEndPoint(IPAddress.Any, 0); udp.Client.ReceiveFrom(buffer, ref remote); endpoints.Add(remote as IPEndPoint); @@ -59,222 +56,212 @@ } }), Task.Delay(snmpTimeOut), - }; - - Task.WaitAny(tasks); - - return endpoints.ToArray(); - } - - /// - /// Gets the name of the public. - /// - /// The host. - /// - /// A string that contains the results of decoding the specified sequence - /// of bytes ref=GetString". - /// - public static string GetPublicName(IPEndPoint host) => GetString(host, "1.3.6.1.2.1.1.5.0"); - - /// - /// Gets the up-time. - /// - /// The host. - /// The mibString. - /// - /// A time interval that represents a specified number of seconds, - /// where the specification is accurate to the nearest millisecond. - /// - public static TimeSpan GetUptime(IPEndPoint host, string mibString = "1.3.6.1.2.1.1.3.0") - { - var response = Get(host, mibString); - if (response[0] == 0xff) return TimeSpan.Zero; - - // If response, get the community name and MIB lengths - var commlength = Convert.ToInt16(response[6]); - var miblength = Convert.ToInt16(response[23 + commlength]); - - // Extract the MIB data from the SNMP response - var datalength = Convert.ToInt16(response[25 + commlength + miblength]); - var datastart = 26 + commlength + miblength; - - var uptime = 0; - - while (datalength > 0) - { - uptime = (uptime << 8) + response[datastart++]; - datalength--; - } - - return TimeSpan.FromSeconds(uptime); - } - - /// - /// Gets the string. - /// - /// The host. - /// The mibString. - /// A that contains the results of decoding the specified sequence of bytes. - public static string GetString(IPEndPoint host, string mibString) - { - var response = Get(host, mibString); - if (response[0] == 0xff) return string.Empty; - - // If response, get the community name and MIB lengths - var commlength = Convert.ToInt16(response[6]); - var miblength = Convert.ToInt16(response[23 + commlength]); - - // Extract the MIB data from the SNMP response - var datalength = Convert.ToInt16(response[25 + commlength + miblength]); - var datastart = 26 + commlength + miblength; - - return Encoding.ASCII.GetString(response, datastart, datalength); - } - - /// - /// Gets the specified host. - /// - /// The host. - /// The mibString. - /// A byte array containing the results of encoding the specified set of characters. - public static byte[] Get(IPEndPoint host, string mibString) => Get("get", host, "public", mibString); - - /// - /// Gets the specified request. - /// - /// The request. - /// The host. - /// The community. - /// The mibString. - /// A byte array containing the results of encoding the specified set of characters. - public static byte[] Get(string request, IPEndPoint host, string community, string mibString) - { - var packet = new byte[1024]; - var mib = new byte[1024]; - var comlen = community.Length; - var mibvals = mibString.Split('.'); - var miblen = mibvals.Length; - var cnt = 0; - var orgmiblen = miblen; - var pos = 0; - - // Convert the string MIB into a byte array of integer values - // Unfortunately, values over 128 require multiple bytes - // which also increases the MIB length - for (var i = 0; i < orgmiblen; i++) - { - int temp = Convert.ToInt16(mibvals[i]); - if (temp > 127) - { - mib[cnt] = Convert.ToByte(128 + (temp / 128)); - mib[cnt + 1] = Convert.ToByte(temp - ((temp / 128) * 128)); - cnt += 2; - miblen++; - } - else - { - mib[cnt] = Convert.ToByte(temp); - cnt++; - } - } - - var snmplen = 29 + comlen + miblen - 1; - - // The SNMP sequence start - packet[pos++] = 0x30; // Sequence start - packet[pos++] = Convert.ToByte(snmplen - 2); // sequence size - - // SNMP version - packet[pos++] = 0x02; // Integer type - packet[pos++] = 0x01; // length - packet[pos++] = 0x00; // SNMP version 1 - - // Community name - packet[pos++] = 0x04; // String type - packet[pos++] = Convert.ToByte(comlen); // length - - // Convert community name to byte array - var data = Encoding.ASCII.GetBytes(community); - - foreach (var t in data) - { - packet[pos++] = t; - } - - // Add GetRequest or GetNextRequest value - if (request == "get") - packet[pos++] = 0xA0; - else - packet[pos++] = 0xA1; - - packet[pos++] = Convert.ToByte(20 + miblen - 1); // Size of total MIB - - // Request ID - packet[pos++] = 0x02; // Integer type - packet[pos++] = 0x04; // length - packet[pos++] = 0x00; // SNMP request ID - packet[pos++] = 0x00; - packet[pos++] = 0x00; - packet[pos++] = 0x01; - - // Error status - packet[pos++] = 0x02; // Integer type - packet[pos++] = 0x01; // length - packet[pos++] = 0x00; // SNMP error status - - // Error index - packet[pos++] = 0x02; // Integer type - packet[pos++] = 0x01; // length - packet[pos++] = 0x00; // SNMP error index - - // Start of variable bindings - packet[pos++] = 0x30; // Start of variable bindings sequence - - packet[pos++] = Convert.ToByte(6 + miblen - 1); // Size of variable binding - - packet[pos++] = 0x30; // Start of first variable bindings sequence - packet[pos++] = Convert.ToByte(6 + miblen - 1 - 2); // size - packet[pos++] = 0x06; // Object type - packet[pos++] = Convert.ToByte(miblen - 1); // length - - // Start of MIB - packet[pos++] = 0x2b; - - // Place MIB array in packet - for (var i = 2; i < miblen; i++) - packet[pos++] = Convert.ToByte(mib[i]); - - packet[pos++] = 0x05; // Null object value - packet[pos] = 0x00; // Null - - // Send packet to destination - SendPacket(host, packet, snmplen); - - return packet; - } - - private static void SendPacket(IPEndPoint host, byte[] packet, int length) - { - var sock = new Socket( - AddressFamily.InterNetwork, - SocketType.Dgram, - ProtocolType.Udp); - sock.SetSocketOption( - SocketOptionLevel.Socket, - SocketOptionName.ReceiveTimeout, - 5000); - var ep = (EndPoint) host; - sock.SendTo(packet, length, SocketFlags.None, host); - - // Receive response from packet - try - { - sock.ReceiveFrom(packet, ref ep); - } - catch (SocketException) - { - packet[0] = 0xff; - } - } - } + }; + + Task.WaitAny(tasks); + + return endpoints.ToArray(); + } + + /// + /// Gets the name of the public. + /// + /// The host. + /// + /// A string that contains the results of decoding the specified sequence + /// of bytes ref=GetString". + /// + public static String GetPublicName(IPEndPoint host) => GetString(host, "1.3.6.1.2.1.1.5.0"); + + /// + /// Gets the up-time. + /// + /// The host. + /// The mibString. + /// + /// A time interval that represents a specified number of seconds, + /// where the specification is accurate to the nearest millisecond. + /// + public static TimeSpan GetUptime(IPEndPoint host, String mibString = "1.3.6.1.2.1.1.3.0") { + Byte[] response = Get(host, mibString); + if(response[0] == 0xff) { + return TimeSpan.Zero; + } + + // If response, get the community name and MIB lengths + Int16 commlength = Convert.ToInt16(response[6]); + Int16 miblength = Convert.ToInt16(response[23 + commlength]); + + // Extract the MIB data from the SNMP response + Int16 datalength = Convert.ToInt16(response[25 + commlength + miblength]); + Int32 datastart = 26 + commlength + miblength; + + Int32 uptime = 0; + + while(datalength > 0) { + uptime = (uptime << 8) + response[datastart++]; + datalength--; + } + + return TimeSpan.FromSeconds(uptime); + } + + /// + /// Gets the string. + /// + /// The host. + /// The mibString. + /// A that contains the results of decoding the specified sequence of bytes. + public static String GetString(IPEndPoint host, String mibString) { + Byte[] response = Get(host, mibString); + if(response[0] == 0xff) { + return String.Empty; + } + + // If response, get the community name and MIB lengths + Int16 commlength = Convert.ToInt16(response[6]); + Int16 miblength = Convert.ToInt16(response[23 + commlength]); + + // Extract the MIB data from the SNMP response + Int16 datalength = Convert.ToInt16(response[25 + commlength + miblength]); + Int32 datastart = 26 + commlength + miblength; + + return Encoding.ASCII.GetString(response, datastart, datalength); + } + + /// + /// Gets the specified host. + /// + /// The host. + /// The mibString. + /// A byte array containing the results of encoding the specified set of characters. + public static Byte[] Get(IPEndPoint host, String mibString) => Get("get", host, "public", mibString); + + /// + /// Gets the specified request. + /// + /// The request. + /// The host. + /// The community. + /// The mibString. + /// A byte array containing the results of encoding the specified set of characters. + public static Byte[] Get(String request, IPEndPoint host, String community, String mibString) { + Byte[] packet = new Byte[1024]; + Byte[] mib = new Byte[1024]; + Int32 comlen = community.Length; + String[] mibvals = mibString.Split('.'); + Int32 miblen = mibvals.Length; + Int32 cnt = 0; + Int32 orgmiblen = miblen; + Int32 pos = 0; + + // Convert the string MIB into a byte array of integer values + // Unfortunately, values over 128 require multiple bytes + // which also increases the MIB length + for(Int32 i = 0; i < orgmiblen; i++) { + Int32 temp = Convert.ToInt16(mibvals[i]); + if(temp > 127) { + mib[cnt] = Convert.ToByte(128 + temp / 128); + mib[cnt + 1] = Convert.ToByte(temp - temp / 128 * 128); + cnt += 2; + miblen++; + } else { + mib[cnt] = Convert.ToByte(temp); + cnt++; + } + } + + Int32 snmplen = 29 + comlen + miblen - 1; + + // The SNMP sequence start + packet[pos++] = 0x30; // Sequence start + packet[pos++] = Convert.ToByte(snmplen - 2); // sequence size + + // SNMP version + packet[pos++] = 0x02; // Integer type + packet[pos++] = 0x01; // length + packet[pos++] = 0x00; // SNMP version 1 + + // Community name + packet[pos++] = 0x04; // String type + packet[pos++] = Convert.ToByte(comlen); // length + + // Convert community name to byte array + Byte[] data = Encoding.ASCII.GetBytes(community); + + foreach(Byte t in data) { + packet[pos++] = t; + } + + // Add GetRequest or GetNextRequest value + packet[pos++] = request == "get" ? (Byte)0xA0 : (Byte)0xA1; + + packet[pos++] = Convert.ToByte(20 + miblen - 1); // Size of total MIB + + // Request ID + packet[pos++] = 0x02; // Integer type + packet[pos++] = 0x04; // length + packet[pos++] = 0x00; // SNMP request ID + packet[pos++] = 0x00; + packet[pos++] = 0x00; + packet[pos++] = 0x01; + + // Error status + packet[pos++] = 0x02; // Integer type + packet[pos++] = 0x01; // length + packet[pos++] = 0x00; // SNMP error status + + // Error index + packet[pos++] = 0x02; // Integer type + packet[pos++] = 0x01; // length + packet[pos++] = 0x00; // SNMP error index + + // Start of variable bindings + packet[pos++] = 0x30; // Start of variable bindings sequence + + packet[pos++] = Convert.ToByte(6 + miblen - 1); // Size of variable binding + + packet[pos++] = 0x30; // Start of first variable bindings sequence + packet[pos++] = Convert.ToByte(6 + miblen - 1 - 2); // size + packet[pos++] = 0x06; // Object type + packet[pos++] = Convert.ToByte(miblen - 1); // length + + // Start of MIB + packet[pos++] = 0x2b; + + // Place MIB array in packet + for(Int32 i = 2; i < miblen; i++) { + packet[pos++] = Convert.ToByte(mib[i]); + } + + packet[pos++] = 0x05; // Null object value + packet[pos] = 0x00; // Null + + // Send packet to destination + SendPacket(host, packet, snmplen); + + return packet; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0067:Objekte verwerfen, bevor Bereich verloren geht", Justification = "")] + private static void SendPacket(IPEndPoint host, Byte[] packet, Int32 length) { + Socket sock = new Socket( + AddressFamily.InterNetwork, + SocketType.Dgram, + ProtocolType.Udp); + sock.SetSocketOption( + SocketOptionLevel.Socket, + SocketOptionName.ReceiveTimeout, + 5000); + EndPoint ep = (EndPoint)host; + _ = sock.SendTo(packet, length, SocketFlags.None, host); + + // Receive response from packet + try { + _ = sock.ReceiveFrom(packet, ref ep); + } catch(SocketException) { + packet[0] = 0xff; + } + } + } } \ No newline at end of file