Init RaspberryIO
This commit is contained in:
@@ -0,0 +1,892 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.IDisposable" />
|
||||
/// <example>
|
||||
/// The following code explains how to create a TCP server.
|
||||
/// <code>
|
||||
/// 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();
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// The following code describes how to create a TCP client.
|
||||
/// <code>
|
||||
/// 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();
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
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<string> _readLineBuffer = new Queue<string>();
|
||||
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<int> _readTask;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Connection"/> class.
|
||||
/// </summary>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <param name="textEncoding">The text encoding.</param>
|
||||
/// <param name="newLineSequence">The new line sequence used for read and write operations.</param>
|
||||
/// <param name="disableContinuousReading">if set to <c>true</c> [disable continuous reading].</param>
|
||||
/// <param name="blockSize">Size of the block. -- set to 0 or less to disable.</param>
|
||||
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;
|
||||
|
||||
#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();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Connection"/> class in continuous reading mode.
|
||||
/// It uses UTF8 encoding, CRLF as a new line sequence and disables a protocol block size.
|
||||
/// </summary>
|
||||
/// <param name="client">The client.</param>
|
||||
public Connection(TcpClient client)
|
||||
: this(client, Encoding.UTF8, "\r\n", false, 0)
|
||||
{
|
||||
// placeholder
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Connection"/> class in continuous reading mode.
|
||||
/// It uses UTF8 encoding, disables line sequences, and uses a protocol block size instead.
|
||||
/// </summary>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <param name="blockSize">Size of the block.</param>
|
||||
public Connection(TcpClient client, int blockSize)
|
||||
: this(client, Encoding.UTF8, new string('\n', blockSize + 1), false, blockSize)
|
||||
{
|
||||
// placeholder
|
||||
}
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the receive buffer has encounters a new line sequence, the buffer is flushed or the buffer is full.
|
||||
/// </summary>
|
||||
public event EventHandler<ConnectionDataReceivedEventArgs> DataReceived = (s, e) => { };
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when an error occurs while upgrading, sending, or receiving data in this client
|
||||
/// </summary>
|
||||
public event EventHandler<ConnectionFailureEventArgs> ConnectionFailure = (s, e) => { };
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a client is disconnected
|
||||
/// </summary>
|
||||
public event EventHandler ClientDisconnected = (s, e) => { };
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique identifier of this connection.
|
||||
/// This field is filled out upon instantiation of this class.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The identifier.
|
||||
/// </value>
|
||||
public Guid Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the active stream. Returns an SSL stream if the connection is secure, otherwise returns
|
||||
/// the underlying NetworkStream.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The active stream.
|
||||
/// </value>
|
||||
public Stream ActiveStream => SecureStream ?? NetworkStream as Stream;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the current connection stream is an SSL stream.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is active stream secure; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool IsActiveStreamSecure => SecureStream != null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the text encoding for send and receive operations.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The text encoding.
|
||||
/// </value>
|
||||
public Encoding TextEncoding { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the remote end point of this TCP connection.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The remote end point.
|
||||
/// </value>
|
||||
public IPEndPoint RemoteEndPoint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the local end point of this TCP connection.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The local end point.
|
||||
/// </value>
|
||||
public IPEndPoint LocalEndPoint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the remote client of this TCP connection.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The remote client.
|
||||
/// </value>
|
||||
public TcpClient RemoteClient { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The size of the protocol block.
|
||||
/// </value>
|
||||
public int ProtocolBlockSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is continuous reading enabled; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool IsContinuousReadingEnabled => _continuousReadingThread != null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the start time at which the connection was started in UTC.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The connection start time UTC.
|
||||
/// </value>
|
||||
public DateTime ConnectionStartTimeUtc { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the start time at which the connection was started in local time.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The connection start time.
|
||||
/// </value>
|
||||
public DateTime ConnectionStartTime => ConnectionStartTimeUtc.ToLocalTime();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the duration of the connection.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The duration of the connection.
|
||||
/// </value>
|
||||
public TimeSpan ConnectionDuration => DateTime.UtcNow.Subtract(ConnectionStartTimeUtc);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last time data was received at in UTC.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The data received last time UTC.
|
||||
/// </value>
|
||||
public DateTime DataReceivedLastTimeUtc { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets how long has elapsed since data was last received.
|
||||
/// </summary>
|
||||
public TimeSpan DataReceivedIdleDuration => DateTime.UtcNow.Subtract(DataReceivedLastTimeUtc);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last time at which data was sent in UTC.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The data sent last time UTC.
|
||||
/// </value>
|
||||
public DateTime DataSentLastTimeUtc { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets how long has elapsed since data was last sent.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The duration of the data sent idle.
|
||||
/// </value>
|
||||
public TimeSpan DataSentIdleDuration => DateTime.UtcNow.Subtract(DataSentLastTimeUtc);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is connected; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Reads data from the remote client asynchronously and with the given timeout.
|
||||
/// </summary>
|
||||
/// <param name="timeout">The timeout.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A byte array containing the results of encoding the specified set of characters.</returns>
|
||||
/// <exception cref="InvalidOperationException">Read methods have been disabled because continuous reading is enabled.</exception>
|
||||
/// <exception cref="TimeoutException">Reading data from {ActiveStream} timed out in {timeout.TotalMilliseconds} m.</exception>
|
||||
public async Task<byte[]> 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<byte>(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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads data asynchronously from the remote stream with a 5000 millisecond timeout.
|
||||
/// </summary>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A byte array containing the results the specified sequence of bytes.</returns>
|
||||
public Task<byte[]> ReadDataAsync(CancellationToken ct = default)
|
||||
=> ReadDataAsync(TimeSpan.FromSeconds(5), ct);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously reads data as text with the given timeout.
|
||||
/// </summary>
|
||||
/// <param name="timeout">The timeout.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A <see cref="System.String" /> that contains the results of decoding the specified sequence of bytes.</returns>
|
||||
public async Task<string> ReadTextAsync(TimeSpan timeout, CancellationToken ct = default)
|
||||
{
|
||||
var buffer = await ReadDataAsync(timeout, ct).ConfigureAwait(false);
|
||||
return buffer == null ? null : TextEncoding.GetString(buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously reads data as text with a 5000 millisecond timeout.
|
||||
/// </summary>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>When this method completes successfully, it returns the contents of the file as a text string.</returns>
|
||||
public Task<string> ReadTextAsync(CancellationToken ct = default)
|
||||
=> ReadTextAsync(TimeSpan.FromSeconds(5), ct);
|
||||
|
||||
/// <summary>
|
||||
/// Performs the same task as this method's overload but it defaults to a read timeout of 30 seconds.
|
||||
/// </summary>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// 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.
|
||||
/// </returns>
|
||||
public Task<string> ReadLineAsync(CancellationToken ct = default)
|
||||
=> ReadLineAsync(TimeSpan.FromSeconds(30), ct);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="timeout">The timeout.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task with a string line from the queue.</returns>
|
||||
/// <exception cref="InvalidOperationException">Read methods have been disabled because continuous reading is enabled.</exception>
|
||||
public async Task<string> 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
|
||||
|
||||
/// <summary>
|
||||
/// Writes data asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer.</param>
|
||||
/// <param name="forceFlush">if set to <c>true</c> [force flush].</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous write operation.</returns>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes text asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous write operation.</returns>
|
||||
public Task WriteTextAsync(string text, CancellationToken ct = default)
|
||||
=> WriteTextAsync(text, TextEncoding, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Writes text asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <param name="encoding">The encoding.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous write operation.</returns>
|
||||
public Task WriteTextAsync(string text, Encoding encoding, CancellationToken ct = default)
|
||||
=> WriteDataAsync(encoding.GetBytes(text), true, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Writes a line of text asynchronously.
|
||||
/// The new line sequence is added automatically at the end of the line.
|
||||
/// </summary>
|
||||
/// <param name="line">The line.</param>
|
||||
/// <param name="encoding">The encoding.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous write operation.</returns>
|
||||
public Task WriteLineAsync(string line, Encoding encoding, CancellationToken ct = default)
|
||||
=> WriteDataAsync(encoding.GetBytes($"{line}{_newLineSequence}"), true, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Writes a line of text asynchronously.
|
||||
/// The new line sequence is added automatically at the end of the line.
|
||||
/// </summary>
|
||||
/// <param name="line">The line.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous write operation.</returns>
|
||||
public Task WriteLineAsync(string line, CancellationToken ct = default)
|
||||
=> WriteLineAsync(line, TextEncoding, ct);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Socket Methods
|
||||
|
||||
/// <summary>
|
||||
/// Upgrades the active stream to an SSL stream if this connection object is hosted in the server.
|
||||
/// </summary>
|
||||
/// <param name="serverCertificate">The server certificate.</param>
|
||||
/// <returns><c>true</c> if the object is hosted in the server; otherwise, <c>false</c>.</returns>
|
||||
public async Task<bool> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upgrades the active stream to an SSL stream if this connection object is hosted in the client.
|
||||
/// </summary>
|
||||
/// <param name="hostname">The hostname.</param>
|
||||
/// <param name="callback">The callback.</param>
|
||||
/// <returns>A tasks with <c>true</c> if the upgrade to SSL was successful; otherwise, <c>false</c>.</returns>
|
||||
public async Task<bool> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnects this connection.
|
||||
/// </summary>
|
||||
public void Disconnect()
|
||||
{
|
||||
if (_disconnectCalls > 0)
|
||||
return;
|
||||
|
||||
_disconnectCalls++;
|
||||
_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();
|
||||
#endif
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
finally
|
||||
{
|
||||
NetworkStream = null;
|
||||
SecureStream = null;
|
||||
RemoteClient = null;
|
||||
_continuousReadingThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Dispose
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_hasDisposed)
|
||||
return;
|
||||
|
||||
// Release managed resources
|
||||
Disconnect();
|
||||
_continuousReadingThread = null;
|
||||
_writeDone.Dispose();
|
||||
|
||||
_hasDisposed = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Continuous Read Methods
|
||||
|
||||
/// <summary>
|
||||
/// Raises the receive buffer events.
|
||||
/// </summary>
|
||||
/// <param name="receivedData">The received data.</param>
|
||||
/// <exception cref="Exception">Split function failed! This is terribly wrong.</exception>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
namespace Unosquare.Swan.Networking
|
||||
{
|
||||
using Swan;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// TCP Listener manager with built-in events and asynchronous functionality.
|
||||
/// This networking component is typically used when writing server software.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.IDisposable" />
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a new connection requests a socket from the listener.
|
||||
/// Set Cancel = true to prevent the TCP client from being accepted.
|
||||
/// </summary>
|
||||
public event EventHandler<ConnectionAcceptingEventArgs> OnConnectionAccepting = (s, e) => { };
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a new connection is accepted.
|
||||
/// </summary>
|
||||
public event EventHandler<ConnectionAcceptedEventArgs> OnConnectionAccepted = (s, e) => { };
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a connection fails to get accepted
|
||||
/// </summary>
|
||||
public event EventHandler<ConnectionFailureEventArgs> OnConnectionFailure = (s, e) => { };
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the listener stops.
|
||||
/// </summary>
|
||||
public event EventHandler<ConnectionListenerStoppedEventArgs> OnListenerStopped = (s, e) => { };
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConnectionListener"/> class.
|
||||
/// </summary>
|
||||
/// <param name="listenEndPoint">The listen end point.</param>
|
||||
public ConnectionListener(IPEndPoint listenEndPoint)
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
LocalEndPoint = listenEndPoint ?? throw new ArgumentNullException(nameof(listenEndPoint));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConnectionListener"/> class.
|
||||
/// It uses the loopback address for listening.
|
||||
/// </summary>
|
||||
/// <param name="listenPort">The listen port.</param>
|
||||
public ConnectionListener(int listenPort)
|
||||
: this(new IPEndPoint(IPAddress.Loopback, listenPort))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConnectionListener"/> class.
|
||||
/// </summary>
|
||||
/// <param name="listenAddress">The listen address.</param>
|
||||
/// <param name="listenPort">The listen port.</param>
|
||||
public ConnectionListener(IPAddress listenAddress, int listenPort)
|
||||
: this(new IPEndPoint(listenAddress, listenPort))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="ConnectionListener"/> class.
|
||||
/// </summary>
|
||||
~ConnectionListener()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the local end point on which we are listening.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The local end point.
|
||||
/// </value>
|
||||
public IPEndPoint LocalEndPoint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this listener is active.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is listening; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool IsListening => _backgroundWorkerTask != null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a unique identifier that gets automatically assigned upon instantiation of this class.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The unique identifier.
|
||||
/// </value>
|
||||
public Guid Id { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Start and Stop
|
||||
|
||||
/// <summary>
|
||||
/// Starts the listener in an asynchronous, non-blocking fashion.
|
||||
/// Subscribe to the events of this class to gain access to connected client sockets.
|
||||
/// </summary>
|
||||
/// <exception cref="System.InvalidOperationException">Cancellation has already been requested. This listener is not reusable.</exception>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the listener from receiving new connections.
|
||||
/// This does not prevent the listener from .
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
_cancellationPending = true;
|
||||
_listenerSocket?.Stop();
|
||||
_cancelListening?.Cancel();
|
||||
_backgroundWorkerTask?.Wait();
|
||||
_backgroundWorkerTask = null;
|
||||
_cancellationPending = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <see cref="System.String" /> that represents this instance.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A <see cref="System.String" /> that represents this instance.
|
||||
/// </returns>
|
||||
public override string ToString() => LocalEndPoint.ToString();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (_hasDisposed)
|
||||
return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
// Release managed resources
|
||||
Stop();
|
||||
}
|
||||
|
||||
_hasDisposed = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Continuously checks for client connections until the Close method has been called.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous connection operation.</returns>
|
||||
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)
|
||||
{
|
||||
#if !NET452
|
||||
client.Dispose();
|
||||
#else
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
namespace Unosquare.Swan.Networking
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// DnsClient public interfaces.
|
||||
/// </summary>
|
||||
internal partial class DnsClient
|
||||
{
|
||||
public interface IDnsMessage
|
||||
{
|
||||
IList<DnsQuestion> 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<IDnsResourceRecord> AnswerRecords { get; }
|
||||
IList<IDnsResourceRecord> AuthorityRecords { get; }
|
||||
IList<IDnsResourceRecord> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,683 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// DnsClient Request inner class.
|
||||
/// </summary>
|
||||
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<DnsQuestion> Questions => _request.Questions;
|
||||
|
||||
public int Size => _request.Size;
|
||||
|
||||
public IPEndPoint Dns { get; set; }
|
||||
|
||||
public byte[] ToArray() => _request.ToArray();
|
||||
|
||||
public override string ToString() => _request.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Resolves this request into a response using the provided DNS information. The given
|
||||
/// request strategy is used to retrieve the response.
|
||||
/// </summary>
|
||||
/// <exception cref="DnsQueryException">Throw if a malformed response is received from the server.</exception>
|
||||
/// <exception cref="IOException">Thrown if a IO error occurs.</exception>
|
||||
/// <exception cref="SocketException">Thrown if a the reading or writing to the socket fails.</exception>
|
||||
/// <returns>The response received from server.</returns>
|
||||
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<DnsQuestion> questions;
|
||||
private DnsHeader header;
|
||||
|
||||
public DnsRequest()
|
||||
{
|
||||
questions = new List<DnsQuestion>();
|
||||
header = new DnsHeader
|
||||
{
|
||||
OperationCode = DnsOperationCode.Query,
|
||||
Response = false,
|
||||
Id = Random.Next(UInt16.MaxValue),
|
||||
};
|
||||
}
|
||||
|
||||
public DnsRequest(IDnsRequest request)
|
||||
{
|
||||
header = new DnsHeader();
|
||||
questions = new List<DnsQuestion>(request.Questions);
|
||||
|
||||
header.Response = false;
|
||||
|
||||
Id = request.Id;
|
||||
OperationCode = request.OperationCode;
|
||||
RecursionDesired = request.RecursionDesired;
|
||||
}
|
||||
|
||||
public IList<DnsQuestion> 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
|
||||
{
|
||||
#if NET452
|
||||
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<byte>();
|
||||
|
||||
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
|
||||
{
|
||||
#if NET452
|
||||
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<DnsHeader>(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<DnsDomain>
|
||||
{
|
||||
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<byte[]>();
|
||||
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<DnsQuestion> GetAllFromArray(byte[] message, int offset, int questionCount) =>
|
||||
GetAllFromArray(message, offset, questionCount, out offset);
|
||||
|
||||
public static IList<DnsQuestion> GetAllFromArray(
|
||||
byte[] message,
|
||||
int offset,
|
||||
int questionCount,
|
||||
out int endOffset)
|
||||
{
|
||||
IList<DnsQuestion> questions = new List<DnsQuestion>(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<Tail>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
namespace Unosquare.Swan.Networking
|
||||
{
|
||||
using Attributes;
|
||||
using Formatters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
/// <summary>
|
||||
/// DnsClient public methods.
|
||||
/// </summary>
|
||||
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<DnsResourceRecord> GetAllFromArray(
|
||||
byte[] message,
|
||||
int offset,
|
||||
int count,
|
||||
out int endOffset)
|
||||
{
|
||||
IList<DnsResourceRecord> records = new List<DnsResourceRecord>(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<Tail>(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<string>(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<string>(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<string>(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<string>(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<string>(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<Options>(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<string>(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<IDnsResourceRecord> GetAllFromArray(
|
||||
byte[] message,
|
||||
int offset,
|
||||
int count,
|
||||
out int endOffset)
|
||||
{
|
||||
var result = new List<IDnsResourceRecord>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
namespace Unosquare.Swan.Networking
|
||||
{
|
||||
using Formatters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
/// <summary>
|
||||
/// DnsClient Response inner class.
|
||||
/// </summary>
|
||||
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<IDnsResourceRecord> AnswerRecords => _response.AnswerRecords;
|
||||
|
||||
public IList<IDnsResourceRecord> AuthorityRecords =>
|
||||
new ReadOnlyCollection<IDnsResourceRecord>(_response.AuthorityRecords);
|
||||
|
||||
public IList<IDnsResourceRecord> AdditionalRecords =>
|
||||
new ReadOnlyCollection<IDnsResourceRecord>(_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<DnsQuestion> Questions => new ReadOnlyCollection<DnsQuestion>(_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<DnsQuestion> questions,
|
||||
IList<IDnsResourceRecord> answers,
|
||||
IList<IDnsResourceRecord> authority,
|
||||
IList<IDnsResourceRecord> additional)
|
||||
{
|
||||
_header = header;
|
||||
Questions = questions;
|
||||
AnswerRecords = answers;
|
||||
AuthorityRecords = authority;
|
||||
AdditionalRecords = additional;
|
||||
}
|
||||
|
||||
public IList<DnsQuestion> Questions { get; }
|
||||
|
||||
public IList<IDnsResourceRecord> AnswerRecords { get; }
|
||||
|
||||
public IList<IDnsResourceRecord> AuthorityRecords { get; }
|
||||
|
||||
public IList<IDnsResourceRecord> 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<IDnsResourceRecord>(),
|
||||
new List<IDnsResourceRecord>(),
|
||||
new List<IDnsResourceRecord>());
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
namespace Unosquare.Swan.Networking
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// DnsClient public methods.
|
||||
/// </summary>
|
||||
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<IPAddress> 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<DnsIPAddressResourceRecord>()
|
||||
.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
namespace Unosquare.Swan.Networking
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a response from a DNS server.
|
||||
/// </summary>
|
||||
public class DnsQueryResult
|
||||
{
|
||||
private readonly List<DnsRecord> m_AnswerRecords = new List<DnsRecord>();
|
||||
private readonly List<DnsRecord> m_AdditionalRecords = new List<DnsRecord>();
|
||||
private readonly List<DnsRecord> m_AuthorityRecords = new List<DnsRecord>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DnsQueryResult"/> class.
|
||||
/// </summary>
|
||||
/// <param name="response">The response.</param>
|
||||
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()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The identifier.
|
||||
/// </value>
|
||||
public int Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is authoritative server.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is authoritative server; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool IsAuthoritativeServer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is truncated.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is truncated; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool IsTruncated { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is recursion available.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is recursion available; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool IsRecursionAvailable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the operation code.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The operation code.
|
||||
/// </value>
|
||||
public DnsOperationCode OperationCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the response code.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The response code.
|
||||
/// </value>
|
||||
public DnsResponseCode ResponseCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the answer records.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The answer records.
|
||||
/// </value>
|
||||
public IList<DnsRecord> AnswerRecords => m_AnswerRecords;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the additional records.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The additional records.
|
||||
/// </value>
|
||||
public IList<DnsRecord> AdditionalRecords => m_AdditionalRecords;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authority records.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The authority records.
|
||||
/// </value>
|
||||
public IList<DnsRecord> AuthorityRecords => m_AuthorityRecords;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
namespace Unosquare.Swan.Networking
|
||||
{
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a DNS record entry.
|
||||
/// </summary>
|
||||
public class DnsRecord
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DnsRecord"/> class.
|
||||
/// </summary>
|
||||
/// <param name="record">The record.</param>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name.
|
||||
/// </value>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The type.
|
||||
/// </value>
|
||||
public DnsRecordType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the class.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The class.
|
||||
/// </value>
|
||||
public DnsRecordClass Class { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the time to live.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The time to live.
|
||||
/// </value>
|
||||
public TimeSpan TimeToLive { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw data of the record.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The data.
|
||||
/// </value>
|
||||
public byte[] Data { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the data text bytes in ASCII encoding.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The data text.
|
||||
/// </value>
|
||||
public string DataText => Data == null ? string.Empty : Encoding.ASCII.GetString(Data);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the pointer domain.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name of the pointer domain.
|
||||
/// </value>
|
||||
public string PointerDomainName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ip address.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The ip address.
|
||||
/// </value>
|
||||
public IPAddress IPAddress { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the name server domain.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name of the name server domain.
|
||||
/// </value>
|
||||
public string NameServerDomainName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the canonical domain.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name of the canonical domain.
|
||||
/// </value>
|
||||
public string CanonicalDomainName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the mail exchanger preference.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The mail exchanger preference.
|
||||
/// </value>
|
||||
public int? MailExchangerPreference { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the mail exchanger domain.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name of the mail exchanger domain.
|
||||
/// </value>
|
||||
public string MailExchangerDomainName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the soa master domain.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name of the soa master domain.
|
||||
/// </value>
|
||||
public string SoaMasterDomainName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the soa responsible domain.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name of the soa responsible domain.
|
||||
/// </value>
|
||||
public string SoaResponsibleDomainName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the soa serial number.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The soa serial number.
|
||||
/// </value>
|
||||
public long? SoaSerialNumber { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the soa refresh interval.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The soa refresh interval.
|
||||
/// </value>
|
||||
public TimeSpan? SoaRefreshInterval { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the soa retry interval.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The soa retry interval.
|
||||
/// </value>
|
||||
public TimeSpan? SoaRetryInterval { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the soa expire interval.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The soa expire interval.
|
||||
/// </value>
|
||||
public TimeSpan? SoaExpireInterval { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the soa minimum time to live.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The soa minimum time to live.
|
||||
/// </value>
|
||||
public TimeSpan? SoaMinimumTimeToLive { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
// ReSharper disable InconsistentNaming
|
||||
namespace Unosquare.Swan.Networking
|
||||
{
|
||||
#region DNS
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the different DNS record types.
|
||||
/// </summary>
|
||||
public enum DnsRecordType
|
||||
{
|
||||
/// <summary>
|
||||
/// A records
|
||||
/// </summary>
|
||||
A = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Nameserver records
|
||||
/// </summary>
|
||||
NS = 2,
|
||||
|
||||
/// <summary>
|
||||
/// CNAME records
|
||||
/// </summary>
|
||||
CNAME = 5,
|
||||
|
||||
/// <summary>
|
||||
/// SOA records
|
||||
/// </summary>
|
||||
SOA = 6,
|
||||
|
||||
/// <summary>
|
||||
/// WKS records
|
||||
/// </summary>
|
||||
WKS = 11,
|
||||
|
||||
/// <summary>
|
||||
/// PTR records
|
||||
/// </summary>
|
||||
PTR = 12,
|
||||
|
||||
/// <summary>
|
||||
/// MX records
|
||||
/// </summary>
|
||||
MX = 15,
|
||||
|
||||
/// <summary>
|
||||
/// TXT records
|
||||
/// </summary>
|
||||
TXT = 16,
|
||||
|
||||
/// <summary>
|
||||
/// A records fot IPv6
|
||||
/// </summary>
|
||||
AAAA = 28,
|
||||
|
||||
/// <summary>
|
||||
/// SRV records
|
||||
/// </summary>
|
||||
SRV = 33,
|
||||
|
||||
/// <summary>
|
||||
/// ANY records
|
||||
/// </summary>
|
||||
ANY = 255,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the different DNS record classes.
|
||||
/// </summary>
|
||||
public enum DnsRecordClass
|
||||
{
|
||||
/// <summary>
|
||||
/// IN records
|
||||
/// </summary>
|
||||
IN = 1,
|
||||
|
||||
/// <summary>
|
||||
/// ANY records
|
||||
/// </summary>
|
||||
ANY = 255,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the different DNS operation codes.
|
||||
/// </summary>
|
||||
public enum DnsOperationCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Query operation
|
||||
/// </summary>
|
||||
Query = 0,
|
||||
|
||||
/// <summary>
|
||||
/// IQuery operation
|
||||
/// </summary>
|
||||
IQuery,
|
||||
|
||||
/// <summary>
|
||||
/// Status operation
|
||||
/// </summary>
|
||||
Status,
|
||||
|
||||
/// <summary>
|
||||
/// Notify operation
|
||||
/// </summary>
|
||||
Notify = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Update operation
|
||||
/// </summary>
|
||||
Update,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the different DNS query response codes.
|
||||
/// </summary>
|
||||
public enum DnsResponseCode
|
||||
{
|
||||
/// <summary>
|
||||
/// No error
|
||||
/// </summary>
|
||||
NoError = 0,
|
||||
|
||||
/// <summary>
|
||||
/// No error
|
||||
/// </summary>
|
||||
FormatError,
|
||||
|
||||
/// <summary>
|
||||
/// Format error
|
||||
/// </summary>
|
||||
ServerFailure,
|
||||
|
||||
/// <summary>
|
||||
/// Server failure error
|
||||
/// </summary>
|
||||
NameError,
|
||||
|
||||
/// <summary>
|
||||
/// Name error
|
||||
/// </summary>
|
||||
NotImplemented,
|
||||
|
||||
/// <summary>
|
||||
/// Not implemented error
|
||||
/// </summary>
|
||||
Refused,
|
||||
|
||||
/// <summary>
|
||||
/// Refused error
|
||||
/// </summary>
|
||||
YXDomain,
|
||||
|
||||
/// <summary>
|
||||
/// YXRR error
|
||||
/// </summary>
|
||||
YXRRSet,
|
||||
|
||||
/// <summary>
|
||||
/// NXRR Set error
|
||||
/// </summary>
|
||||
NXRRSet,
|
||||
|
||||
/// <summary>
|
||||
/// Not authorized error
|
||||
/// </summary>
|
||||
NotAuth,
|
||||
|
||||
/// <summary>
|
||||
/// Not zone error
|
||||
/// </summary>
|
||||
NotZone,
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
// ReSharper disable InconsistentNaming
|
||||
namespace Unosquare.Swan.Networking
|
||||
{
|
||||
#if NETSTANDARD1_3
|
||||
|
||||
/// <summary>
|
||||
/// Defines the different SMTP status codes
|
||||
/// </summary>
|
||||
public enum SmtpStatusCode
|
||||
{
|
||||
/// <summary>
|
||||
/// System code
|
||||
/// </summary>
|
||||
SystemStatus = 211,
|
||||
|
||||
/// <summary>
|
||||
/// Help message code
|
||||
/// </summary>
|
||||
HelpMessage = 214,
|
||||
|
||||
/// <summary>
|
||||
/// Service ready code
|
||||
/// </summary>
|
||||
ServiceReady = 220,
|
||||
|
||||
/// <summary>
|
||||
/// Service closing channel code
|
||||
/// </summary>
|
||||
ServiceClosingTransmissionChannel = 221,
|
||||
|
||||
/// <summary>
|
||||
/// OK Code
|
||||
/// </summary>
|
||||
Ok = 250,
|
||||
|
||||
/// <summary>
|
||||
/// User not local code
|
||||
/// </summary>
|
||||
UserNotLocalWillForward = 251,
|
||||
|
||||
/// <summary>
|
||||
/// Cannot verify user code
|
||||
/// </summary>
|
||||
CannotVerifyUserWillAttemptDelivery = 252,
|
||||
|
||||
/// <summary>
|
||||
/// Start Mail Input code
|
||||
/// </summary>
|
||||
StartMailInput = 354,
|
||||
|
||||
/// <summary>
|
||||
/// Service Not Available code
|
||||
/// </summary>
|
||||
ServiceNotAvailable = 421,
|
||||
|
||||
/// <summary>
|
||||
/// Mailbox Busy code
|
||||
/// </summary>
|
||||
MailboxBusy = 450,
|
||||
|
||||
/// <summary>
|
||||
/// Local Error code
|
||||
/// </summary>
|
||||
LocalErrorInProcessing = 451,
|
||||
|
||||
/// <summary>
|
||||
/// Insufficient storage code
|
||||
/// </summary>
|
||||
InsufficientStorage = 452,
|
||||
|
||||
/// <summary>
|
||||
/// Client not permitted code
|
||||
/// </summary>
|
||||
ClientNotPermitted = 454,
|
||||
|
||||
/// <summary>
|
||||
/// Command Unrecognized
|
||||
/// </summary>
|
||||
CommandUnrecognized = 500,
|
||||
|
||||
/// <summary>
|
||||
/// Syntax error
|
||||
/// </summary>
|
||||
SyntaxError = 501,
|
||||
|
||||
/// <summary>
|
||||
/// Command Not Implemented
|
||||
/// </summary>
|
||||
CommandNotImplemented = 502,
|
||||
|
||||
/// <summary>
|
||||
/// Bad Command Sequence
|
||||
/// </summary>
|
||||
BadCommandSequence = 503,
|
||||
|
||||
/// <summary>
|
||||
/// Must Issue Start Tls First
|
||||
/// </summary>
|
||||
MustIssueStartTlsFirst = 530,
|
||||
|
||||
/// <summary>
|
||||
/// Command Parameter Not Implemented
|
||||
/// </summary>
|
||||
CommandParameterNotImplemented = 504,
|
||||
|
||||
/// <summary>
|
||||
/// Mailbox Unavailable
|
||||
/// </summary>
|
||||
MailboxUnavailable = 550,
|
||||
|
||||
/// <summary>
|
||||
/// User Not Local Try Alternate Path
|
||||
/// </summary>
|
||||
UserNotLocalTryAlternatePath = 551,
|
||||
|
||||
/// <summary>
|
||||
/// Exceeded Storage Allocation code
|
||||
/// </summary>
|
||||
ExceededStorageAllocation = 552,
|
||||
|
||||
/// <summary>
|
||||
/// Mailbox name not allowed code
|
||||
/// </summary>
|
||||
MailboxNameNotAllowed = 553,
|
||||
|
||||
/// <summary>
|
||||
/// Transaction failed code
|
||||
/// </summary>
|
||||
TransactionFailed = 554,
|
||||
|
||||
/// <summary>
|
||||
/// General Failure code
|
||||
/// </summary>
|
||||
GeneralFailure = -1,
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates all of the well-known SMTP command names.
|
||||
/// </summary>
|
||||
public enum SmtpCommandNames
|
||||
{
|
||||
/// <summary>
|
||||
/// An unknown command
|
||||
/// </summary>
|
||||
Unknown,
|
||||
|
||||
/// <summary>
|
||||
/// The helo command
|
||||
/// </summary>
|
||||
HELO,
|
||||
|
||||
/// <summary>
|
||||
/// The ehlo command
|
||||
/// </summary>
|
||||
EHLO,
|
||||
|
||||
/// <summary>
|
||||
/// The quit command
|
||||
/// </summary>
|
||||
QUIT,
|
||||
|
||||
/// <summary>
|
||||
/// The help command
|
||||
/// </summary>
|
||||
HELP,
|
||||
|
||||
/// <summary>
|
||||
/// The noop command
|
||||
/// </summary>
|
||||
NOOP,
|
||||
|
||||
/// <summary>
|
||||
/// The rset command
|
||||
/// </summary>
|
||||
RSET,
|
||||
|
||||
/// <summary>
|
||||
/// The mail command
|
||||
/// </summary>
|
||||
MAIL,
|
||||
|
||||
/// <summary>
|
||||
/// The data command
|
||||
/// </summary>
|
||||
DATA,
|
||||
|
||||
/// <summary>
|
||||
/// The send command
|
||||
/// </summary>
|
||||
SEND,
|
||||
|
||||
/// <summary>
|
||||
/// The soml command
|
||||
/// </summary>
|
||||
SOML,
|
||||
|
||||
/// <summary>
|
||||
/// The saml command
|
||||
/// </summary>
|
||||
SAML,
|
||||
|
||||
/// <summary>
|
||||
/// The RCPT command
|
||||
/// </summary>
|
||||
RCPT,
|
||||
|
||||
/// <summary>
|
||||
/// The vrfy command
|
||||
/// </summary>
|
||||
VRFY,
|
||||
|
||||
/// <summary>
|
||||
/// The expn command
|
||||
/// </summary>
|
||||
EXPN,
|
||||
|
||||
/// <summary>
|
||||
/// The starttls command
|
||||
/// </summary>
|
||||
STARTTLS,
|
||||
|
||||
/// <summary>
|
||||
/// The authentication command
|
||||
/// </summary>
|
||||
AUTH,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the reply code severities.
|
||||
/// </summary>
|
||||
public enum SmtpReplyCodeSeverities
|
||||
{
|
||||
/// <summary>
|
||||
/// The unknown severity
|
||||
/// </summary>
|
||||
Unknown = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The positive completion severity
|
||||
/// </summary>
|
||||
PositiveCompletion = 200,
|
||||
|
||||
/// <summary>
|
||||
/// The positive intermediate severity
|
||||
/// </summary>
|
||||
PositiveIntermediate = 300,
|
||||
|
||||
/// <summary>
|
||||
/// The transient negative severity
|
||||
/// </summary>
|
||||
TransientNegative = 400,
|
||||
|
||||
/// <summary>
|
||||
/// The permanent negative severity
|
||||
/// </summary>
|
||||
PermanentNegative = 500,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the reply code categories.
|
||||
/// </summary>
|
||||
public enum SmtpReplyCodeCategories
|
||||
{
|
||||
/// <summary>
|
||||
/// The unknown category
|
||||
/// </summary>
|
||||
Unknown = -1,
|
||||
|
||||
/// <summary>
|
||||
/// The syntax category
|
||||
/// </summary>
|
||||
Syntax = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The information category
|
||||
/// </summary>
|
||||
Information = 1,
|
||||
|
||||
/// <summary>
|
||||
/// The connections category
|
||||
/// </summary>
|
||||
Connections = 2,
|
||||
|
||||
/// <summary>
|
||||
/// The unspecified a category
|
||||
/// </summary>
|
||||
UnspecifiedA = 3,
|
||||
|
||||
/// <summary>
|
||||
/// The unspecified b category
|
||||
/// </summary>
|
||||
UnspecifiedB = 4,
|
||||
|
||||
/// <summary>
|
||||
/// The system category
|
||||
/// </summary>
|
||||
System = 5,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a HttpClient with extended methods to use with JSON payloads
|
||||
/// and bearer tokens authentication.
|
||||
/// </summary>
|
||||
public static class JsonClient
|
||||
{
|
||||
private const string JsonMimeType = "application/json";
|
||||
|
||||
/// <summary>
|
||||
/// Post a object as JSON with optional authorization token.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of response object.</typeparam>
|
||||
/// <param name="url">The URL.</param>
|
||||
/// <param name="payload">The payload.</param>
|
||||
/// <param name="authorization">The authorization.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task with a result of the requested type.</returns>
|
||||
public static async Task<T> Post<T>(
|
||||
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<T>(jsonString) : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts a object as JSON with optional authorization token and retrieve an object
|
||||
/// or an error.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of response object.</typeparam>
|
||||
/// <typeparam name="TE">The type of the error.</typeparam>
|
||||
/// <param name="url">The URL.</param>
|
||||
/// <param name="payload">The payload.</param>
|
||||
/// <param name="httpStatusError">The HTTP status error.</param>
|
||||
/// <param name="authorization">The authorization.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task with a result of the requested type or an error object.</returns>
|
||||
public static async Task<OkOrError<T, TE>> PostOrError<T, TE>(
|
||||
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<T, TE>.FromOk(!string.IsNullOrEmpty(jsonString)
|
||||
? Json.Deserialize<T>(jsonString)
|
||||
: default);
|
||||
}
|
||||
|
||||
if ((int) response.StatusCode == httpStatusError)
|
||||
{
|
||||
return OkOrError<T, TE>.FromError(!string.IsNullOrEmpty(jsonString)
|
||||
? Json.Deserialize<TE>(jsonString)
|
||||
: default);
|
||||
}
|
||||
|
||||
return new OkOrError<T, TE>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts the specified URL.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL.</param>
|
||||
/// <param name="payload">The payload.</param>
|
||||
/// <param name="authorization">The authorization.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task with a result as a collection of key/value pairs.</returns>
|
||||
public static async Task<IDictionary<string, object>> 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<string, object>;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts the specified URL.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL.</param>
|
||||
/// <param name="payload">The payload.</param>
|
||||
/// <param name="authorization">The authorization.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// A task with a result of the requested string.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">url.</exception>
|
||||
/// <exception cref="JsonRequestException">Error POST JSON.</exception>
|
||||
public static Task<string> PostString(
|
||||
string url,
|
||||
object payload,
|
||||
string authorization = null,
|
||||
CancellationToken ct = default) => SendAsync(HttpMethod.Post, url, payload, authorization, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Puts the specified URL.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of response object.</typeparam>
|
||||
/// <param name="url">The URL.</param>
|
||||
/// <param name="payload">The payload.</param>
|
||||
/// <param name="authorization">The authorization.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task with a result of the requested type.</returns>
|
||||
public static async Task<T> Put<T>(
|
||||
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<T>(jsonString) : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Puts the specified URL.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL.</param>
|
||||
/// <param name="payload">The payload.</param>
|
||||
/// <param name="authorization">The authorization.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task with a result of the requested collection of key/value pairs.</returns>
|
||||
public static async Task<IDictionary<string, object>> Put(
|
||||
string url,
|
||||
object payload,
|
||||
string authorization = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var response = await Put<object>(url, payload, authorization, ct).ConfigureAwait(false);
|
||||
|
||||
return response as IDictionary<string, object>;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Puts as string.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL.</param>
|
||||
/// <param name="payload">The payload.</param>
|
||||
/// <param name="authorization">The authorization.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// A task with a result of the requested string.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">url.</exception>
|
||||
/// <exception cref="JsonRequestException">Error PUT JSON.</exception>
|
||||
public static Task<string> PutString(
|
||||
string url,
|
||||
object payload,
|
||||
string authorization = null,
|
||||
CancellationToken ct = default) => SendAsync(HttpMethod.Put, url, payload, authorization, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Gets as string.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL.</param>
|
||||
/// <param name="authorization">The authorization.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// A task with a result of the requested string.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">url.</exception>
|
||||
/// <exception cref="JsonRequestException">Error GET JSON.</exception>
|
||||
public static async Task<string> GetString(
|
||||
string url,
|
||||
string authorization = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var response = await GetHttpContent(url, authorization, ct).ConfigureAwait(false);
|
||||
|
||||
return await response.ReadAsStringAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the specified URL and return the JSON data as object
|
||||
/// with optional authorization token.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The response type.</typeparam>
|
||||
/// <param name="url">The URL.</param>
|
||||
/// <param name="authorization">The authorization.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task with a result of the requested type.</returns>
|
||||
public static async Task<T> Get<T>(
|
||||
string url,
|
||||
string authorization = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var jsonString = await GetString(url, authorization, ct).ConfigureAwait(false);
|
||||
|
||||
return !string.IsNullOrEmpty(jsonString) ? Json.Deserialize<T>(jsonString) : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the binary.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL.</param>
|
||||
/// <param name="authorization">The authorization.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// A task with a result of the requested byte array.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">url.</exception>
|
||||
/// <exception cref="JsonRequestException">Error GET Binary.</exception>
|
||||
public static async Task<byte[]> GetBinary(
|
||||
string url,
|
||||
string authorization = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var response = await GetHttpContent(url, authorization, ct).ConfigureAwait(false);
|
||||
|
||||
return await response.ReadAsByteArrayAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authenticate against a web server using Bearer Token.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL.</param>
|
||||
/// <param name="username">The username.</param>
|
||||
/// <param name="password">The password.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// A task with a Dictionary with authentication data.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// url
|
||||
/// or
|
||||
/// username.
|
||||
/// </exception>
|
||||
/// <exception cref="SecurityException">Error Authenticating.</exception>
|
||||
public static async Task<IDictionary<string, object>> 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(
|
||||
$"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<string, object>;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts the file.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL.</param>
|
||||
/// <param name="buffer">The buffer.</param>
|
||||
/// <param name="fileName">Name of the file.</param>
|
||||
/// <param name="authorization">The authorization.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// A task with a result of the requested string.
|
||||
/// </returns>
|
||||
public static Task<string> PostFileString(
|
||||
string url,
|
||||
byte[] buffer,
|
||||
string fileName,
|
||||
string authorization = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return PostString(url, new {Filename = fileName, Data = buffer}, authorization, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts the file.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The response type.</typeparam>
|
||||
/// <param name="url">The URL.</param>
|
||||
/// <param name="buffer">The buffer.</param>
|
||||
/// <param name="fileName">Name of the file.</param>
|
||||
/// <param name="authorization">The authorization.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task with a result of the requested string.</returns>
|
||||
public static Task<T> PostFile<T>(
|
||||
string url,
|
||||
byte[] buffer,
|
||||
string fileName,
|
||||
string authorization = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return Post<T>(url, new {Filename = fileName, Data = buffer}, authorization, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the asynchronous request.
|
||||
/// </summary>
|
||||
/// <param name="method">The method.</param>
|
||||
/// <param name="url">The URL.</param>
|
||||
/// <param name="payload">The payload.</param>
|
||||
/// <param name="authorization">The authorization.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task with a result of the requested string.</returns>
|
||||
public static async Task<string> 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<HttpContent> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,621 @@
|
||||
namespace Unosquare.Swan.Networking.Ldap
|
||||
{
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <seealso cref="Asn1Structured" />
|
||||
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: { ");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Asn1Choice object represents the choice of any Asn1Object. All
|
||||
/// Asn1Object methods are delegated to the object this Asn1Choice contains.
|
||||
/// </summary>
|
||||
/// <seealso cref="Asn1Object" />
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class is used to encapsulate an ASN.1 Identifier.
|
||||
/// An Asn1Identifier is composed of three parts:
|
||||
/// <li> a class type,</li><li> a form, and</li><li> a tag.</li>
|
||||
/// The class type is defined as:
|
||||
/// <pre>
|
||||
/// bit 8 7 TAG CLASS
|
||||
/// ------- -----------
|
||||
/// 0 0 UNIVERSAL
|
||||
/// 0 1 APPLICATION
|
||||
/// 1 0 CONTEXT
|
||||
/// 1 1 PRIVATE
|
||||
/// </pre>
|
||||
/// The form is defined as:
|
||||
/// <pre>
|
||||
/// bit 6 FORM
|
||||
/// ----- --------
|
||||
/// 0 PRIMITIVE
|
||||
/// 1 CONSTRUCTED
|
||||
/// </pre>
|
||||
/// Note: CONSTRUCTED types are made up of other CONSTRUCTED or PRIMITIVE
|
||||
/// types.
|
||||
/// The tag is defined as:.
|
||||
/// <pre>
|
||||
/// 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
|
||||
/// </pre></summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is the base class for all other Asn1 types.
|
||||
/// </summary>
|
||||
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}]";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class encapsulates the OCTET STRING type.
|
||||
/// </summary>
|
||||
/// <seealso cref="Asn1Object" />
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <seealso cref="Asn1Object" />
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class serves as the base type for all ASN.1
|
||||
/// structured types.
|
||||
/// </summary>
|
||||
/// <seealso cref="Asn1Object" />
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class encapsulates the ASN.1 BOOLEAN type.
|
||||
/// </summary>
|
||||
/// <seealso cref="Asn1Object" />
|
||||
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}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class represents the ASN.1 NULL type.
|
||||
/// </summary>
|
||||
/// <seealso cref="Asn1Object" />
|
||||
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: \"\"";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This abstract class is the base class
|
||||
/// for all Asn1 numeric (integral) types. These include
|
||||
/// Asn1Integer and Asn1Enumerated.
|
||||
/// </summary>
|
||||
/// <seealso cref="Asn1Object" />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class provides a means to manipulate ASN.1 Length's. It will
|
||||
/// be used by Asn1Encoder's and Asn1Decoder's by composition.
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <seealso cref="Asn1Structured" />
|
||||
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: { ");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <seealso cref="Asn1Structured" />
|
||||
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: { ");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class encapsulates the ASN.1 INTEGER type.
|
||||
/// </summary>
|
||||
/// <seealso cref="Asn1Numeric" />
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class encapsulates the ASN.1 ENUMERATED type.
|
||||
/// </summary>
|
||||
/// <seealso cref="Asn1Numeric" />
|
||||
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)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Asn1Enumerated"/> class.
|
||||
/// Constructs an Asn1Enumerated object by decoding data from an
|
||||
/// input stream.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream.</param>
|
||||
/// <param name="len">The length.</param>
|
||||
public Asn1Enumerated(Stream stream, int len)
|
||||
: base(Id, LberDecoder.DecodeNumeric(stream, len))
|
||||
{
|
||||
}
|
||||
|
||||
public override string ToString() => base.ToString() + "ENUMERATED: " + LongValue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <seealso cref="Asn1Structured" />
|
||||
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: { ");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
namespace Unosquare.Swan.Networking.Ldap
|
||||
{
|
||||
using System.IO;
|
||||
|
||||
/// <summary>
|
||||
/// 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:
|
||||
/// <li>(1) Only the definite form of length encoding will be used.</li>
|
||||
/// <li>(2) OCTET STRING values will be encoded in the primitive form only.</li><li>
|
||||
/// (3) If the value of a BOOLEAN type is true, the encoding MUST have
|
||||
/// its contents octets set to hex "FF".
|
||||
/// </li><li>
|
||||
/// (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.
|
||||
/// </li>
|
||||
/// [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.
|
||||
/// </summary>
|
||||
internal static class LberDecoder
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream.</param>
|
||||
/// <param name="len">The length.</param>
|
||||
/// <returns>
|
||||
/// Decoded Asn1Obect.
|
||||
/// </returns>
|
||||
/// <exception cref="EndOfStreamException">Unknown tag.</exception>
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decode a boolean directly from a stream.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream.</param>
|
||||
/// <param name="len">Length in bytes.</param>
|
||||
/// <returns>
|
||||
/// Decoded boolean object.
|
||||
/// </returns>
|
||||
/// <exception cref="EndOfStreamException">LBER: BOOLEAN: decode error: EOF.</exception>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decode a Numeric type directly from a stream. Decodes INTEGER
|
||||
/// and ENUMERATED types.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream.</param>
|
||||
/// <param name="len">Length in bytes.</param>
|
||||
/// <returns>
|
||||
/// Decoded numeric object.
|
||||
/// </returns>
|
||||
/// <exception cref="EndOfStreamException">
|
||||
/// LBER: NUMERIC: decode error: EOF
|
||||
/// or
|
||||
/// LBER: NUMERIC: decode error: EOF.
|
||||
/// </exception>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decode an OctetString directly from a stream.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream.</param>
|
||||
/// <param name="len">Length in bytes.</param>
|
||||
/// <returns>Decoded octet. </returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
namespace Unosquare.Swan.Networking.Ldap
|
||||
{
|
||||
using System.IO;
|
||||
|
||||
/// <summary>
|
||||
/// 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:
|
||||
/// <li>(1) Only the definite form of length encoding will be used.</li>
|
||||
/// <li>(2) OCTET STRING values will be encoded in the primitive form only.</li><li>
|
||||
/// (3) If the value of a BOOLEAN type is true, the encoding MUST have
|
||||
/// its contents octets set to hex "FF".
|
||||
/// </li><li>
|
||||
/// (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.
|
||||
/// </li>
|
||||
/// [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.
|
||||
/// </summary>
|
||||
internal static class LberEncoder
|
||||
{
|
||||
/// <summary>
|
||||
/// BER Encode an Asn1Boolean directly into the specified output stream.
|
||||
/// </summary>
|
||||
/// <param name="b">The Asn1Boolean object to encode.</param>
|
||||
/// <param name="stream">The stream.</param>
|
||||
public static void Encode(Asn1Boolean b, Stream stream)
|
||||
{
|
||||
Encode(b.GetIdentifier(), stream);
|
||||
stream.WriteByte(0x01);
|
||||
stream.WriteByte((byte) (b.BooleanValue() ? 0xff : 0x00));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="n">The Asn1Numeric object to encode.</param>
|
||||
/// <param name="stream">The stream.</param>
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encode an Asn1OctetString directly into the specified outputstream.
|
||||
/// </summary>
|
||||
/// <param name="os">The Asn1OctetString object to encode.</param>
|
||||
/// <param name="stream">The stream.</param>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encode an Asn1Structured into the specified outputstream. This method
|
||||
/// can be used to encode SET, SET_OF, SEQUENCE, SEQUENCE_OF.
|
||||
/// </summary>
|
||||
/// <param name="c">The Asn1Structured object to encode.</param>
|
||||
/// <param name="stream">The stream.</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encode an Asn1Tagged directly into the specified outputstream.
|
||||
/// </summary>
|
||||
/// <param name="t">The Asn1Tagged object to encode.</param>
|
||||
/// <param name="stream">The stream.</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encode an Asn1Identifier directly into the specified outputstream.
|
||||
/// </summary>
|
||||
/// <param name="id">The Asn1Identifier object to encode.</param>
|
||||
/// <param name="stream">The stream.</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes the length.
|
||||
/// </summary>
|
||||
/// <param name="length">The length.</param>
|
||||
/// <param name="stream">The stream.</param>
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes the provided tag into the stream.
|
||||
/// </summary>
|
||||
/// <param name="val">The value.</param>
|
||||
/// <param name="stream">The stream.</param>
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// The following code describes how to use the LdapConnection class:
|
||||
///
|
||||
/// <code>
|
||||
/// 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();
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class LdapConnection : IDisposable
|
||||
{
|
||||
private const int LdapV3 = 3;
|
||||
|
||||
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
|
||||
|
||||
private Connection _conn;
|
||||
private bool _isDisposing;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the protocol version uses to authenticate.
|
||||
/// 0 is returned if no authentication has been performed.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The protocol version.
|
||||
/// </value>
|
||||
public int ProtocolVersion => BindProperties?.ProtocolVersion ?? LdapV3;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The authentication dn.
|
||||
/// </value>
|
||||
public string AuthenticationDn => BindProperties == null ? null : (BindProperties.Anonymous ? null : BindProperties.AuthenticationDN);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the method used to authenticate the connection. The return
|
||||
/// value is one of the following:.
|
||||
/// <ul><li>"none" indicates the connection is not authenticated.</li><li>
|
||||
/// "simple" indicates simple authentication was used or that a null
|
||||
/// or empty authentication DN was specified.
|
||||
/// </li><li>"sasl" indicates that a SASL mechanism was used to authenticate</li></ul>
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The authentication method.
|
||||
/// </value>
|
||||
public string AuthenticationMethod => BindProperties == null ? "simple" : BindProperties.AuthenticationMethod;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the connection represented by this object is open
|
||||
/// at this time.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// True if connection is open; false if the connection is closed.
|
||||
/// </returns>
|
||||
public bool Connected => _conn?.IsConnected == true;
|
||||
|
||||
internal BindProperties BindProperties { get; set; }
|
||||
|
||||
internal List<RfcLdapMessage> Messages { get; } = new List<RfcLdapMessage>();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposing) return;
|
||||
|
||||
_isDisposing = true;
|
||||
Disconnect();
|
||||
_cts?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="dn">If non-null and non-empty, specifies that the
|
||||
/// connection and all operations through it should
|
||||
/// be authenticated with dn as the distinguished
|
||||
/// name.</param>
|
||||
/// <param name="password">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.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="Task" /> representing the asynchronous operation.
|
||||
/// </returns>
|
||||
public Task Bind(string dn, string password) => Bind(LdapV3, dn, password);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="version">The Ldap protocol version, use Ldap_V3.
|
||||
/// Ldap_V2 is not supported.</param>
|
||||
/// <param name="dn">If non-null and non-empty, specifies that the
|
||||
/// connection and all operations through it should
|
||||
/// be authenticated with dn as the distinguished
|
||||
/// name.</param>
|
||||
/// <param name="password">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.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="host">A host name or a dotted string representing the IP address
|
||||
/// of a host running an Ldap server.</param>
|
||||
/// <param name="port">The TCP or UDP port number to connect to or contact.
|
||||
/// The default Ldap port is 389.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
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);
|
||||
|
||||
#pragma warning disable 4014
|
||||
Task.Run(() => RetrieveMessages(), _cts.Token);
|
||||
#pragma warning restore 4014
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public void Disconnect()
|
||||
{
|
||||
// disconnect from API call
|
||||
_cts.Cancel();
|
||||
_conn.Disconnect();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Synchronously reads the entry for the specified distinguished name (DN),
|
||||
/// using the specified constraints, and retrieves only the specified
|
||||
/// attributes from the entry.
|
||||
/// </summary>
|
||||
/// <param name="dn">The distinguished name of the entry to retrieve.</param>
|
||||
/// <param name="attrs">The names of the attributes to retrieve.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// the LdapEntry read from the server.
|
||||
/// </returns>
|
||||
/// <exception cref="LdapException">Read response is ambiguous, multiple entries returned.</exception>
|
||||
public async Task<LdapEntry> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
/// <param name="base">The base distinguished name to search from.</param>
|
||||
/// <param name="scope">The scope of the entries to search.</param>
|
||||
/// <param name="filter">The search filter specifying the search criteria.</param>
|
||||
/// <param name="attrs">The names of attributes to retrieve.</param>
|
||||
/// <param name="typesOnly">If true, returns the names but not the values of
|
||||
/// the attributes found. If false, returns the
|
||||
/// names and values for attributes found.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="Task" /> representing the asynchronous operation.
|
||||
/// </returns>
|
||||
public async Task<LdapSearchResults> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Modifies the specified dn.
|
||||
/// </summary>
|
||||
/// <param name="distinguishedName">Name of the distinguished.</param>
|
||||
/// <param name="mods">The mods.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="Task" /> representing the asynchronous operation.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">distinguishedName.</exception>
|
||||
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<RfcLdapMessage>(Messages).Any(x => x.MessageId == msg.MessageId) == false)
|
||||
await Task.Delay(100, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// expected
|
||||
}
|
||||
|
||||
var first = new List<RfcLdapMessage>(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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
namespace Unosquare.Swan.Networking.Ldap
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class LdapControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LdapControl"/> class.
|
||||
/// Constructs a new LdapControl object using the specified values.
|
||||
/// </summary>
|
||||
/// <param name="oid">The OID of the control, as a dotted string.</param>
|
||||
/// <param name="critical">True if the Ldap operation should be discarded if
|
||||
/// the control is not supported. False if
|
||||
/// the operation can be processed without the control.</param>
|
||||
/// <param name="values">The control-specific data.</param>
|
||||
/// <exception cref="ArgumentException">An OID must be specified.</exception>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the identifier of the control.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The identifier.
|
||||
/// </value>
|
||||
public string Id => Asn1Object.ControlType.StringValue();
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether the control is critical for the operation.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if critical; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool Critical => Asn1Object.Criticality.BooleanValue();
|
||||
|
||||
internal static RespControlVector RegisteredControls { get; } = new RespControlVector(5);
|
||||
|
||||
internal RfcControl Asn1Object { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="oid">The object identifier of the control.</param>
|
||||
/// <param name="controlClass">A class which can instantiate an LdapControl.</param>
|
||||
public static void Register(string oid, Type controlClass)
|
||||
=> RegisteredControls.RegisterResponseControl(oid, controlClass);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the control-specific data of the object.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The control-specific data of the object as a byte array,
|
||||
/// or null if the control has no data.
|
||||
/// </returns>
|
||||
public sbyte[] GetValue() => Asn1Object.ControlValue?.ByteValue();
|
||||
|
||||
internal void SetValue(sbyte[] controlValue)
|
||||
{
|
||||
Asn1Object.ControlValue = new Asn1OctetString(controlValue);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a simple bind request.
|
||||
/// </summary>
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.LdapMessage" />
|
||||
public class LdapBindRequest : LdapMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LdapBindRequest"/> class.
|
||||
/// Constructs a simple bind request.
|
||||
/// </summary>
|
||||
/// <param name="version">The Ldap protocol version, use Ldap_V3.
|
||||
/// Ldap_V2 is not supported.</param>
|
||||
/// <param name="dn">If non-null and non-empty, specifies that the
|
||||
/// connection and all operations through it should
|
||||
/// be authenticated with dn as the distinguished
|
||||
/// name.</param>
|
||||
/// <param name="password">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.</param>
|
||||
public LdapBindRequest(int version, string dn, sbyte[] password)
|
||||
: base(LdapOperation.BindRequest, new RfcBindRequest(version, dn, password))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the Authentication DN for a bind request.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The authentication dn.
|
||||
/// </value>
|
||||
public string AuthenticationDN => Asn1Object.RequestDn;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => Asn1Object.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encapsulates a continuation reference from an asynchronous search operation.
|
||||
/// </summary>
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.LdapMessage" />
|
||||
internal class LdapSearchResultReference : LdapMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LdapSearchResultReference"/> class.
|
||||
/// Constructs an LdapSearchResultReference object.
|
||||
/// </summary>
|
||||
/// <param name="message">The LdapMessage with a search reference.</param>
|
||||
internal LdapSearchResultReference(RfcLdapMessage message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns any URLs in the object.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The referrals.
|
||||
/// </value>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The RespControlVector class implements extends the
|
||||
/// existing Vector class so that it can be used to maintain a
|
||||
/// list of currently registered control responses.
|
||||
/// </summary>
|
||||
internal class RespControlVector : List<RespControlVector.RegisteredControl>
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inner class defined to create a temporary object to encapsulate
|
||||
/// all registration information about a response control.
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents and Ldap Bind Request.
|
||||
/// <pre>
|
||||
/// BindRequest ::= [APPLICATION 0] SEQUENCE {
|
||||
/// version INTEGER (1 .. 127),
|
||||
/// name LdapDN,
|
||||
/// authentication AuthenticationChoice }
|
||||
/// </pre></summary>
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
|
||||
/// <seealso cref="IRfcRequest" />
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,794 @@
|
||||
namespace Unosquare.Swan.Networking.Ldap
|
||||
{
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <seealso cref="LdapAttribute"></seealso>
|
||||
/// <seealso cref="LdapAttributeSet"></seealso>
|
||||
public class LdapEntry
|
||||
{
|
||||
private readonly LdapAttributeSet _attrs;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LdapEntry" /> class.
|
||||
/// Constructs a new entry with the specified distinguished name and set
|
||||
/// of attributes.
|
||||
/// </summary>
|
||||
/// <param name="dn">The distinguished name of the new entry. The
|
||||
/// value is not validated. An invalid distinguished
|
||||
/// name will cause operations using this entry to fail.</param>
|
||||
/// <param name="attrs">The initial set of attributes assigned to the
|
||||
/// entry.</param>
|
||||
public LdapEntry(string dn = null, LdapAttributeSet attrs = null)
|
||||
{
|
||||
DN = dn ?? string.Empty;
|
||||
_attrs = attrs ?? new LdapAttributeSet();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the distinguished name of the entry.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The dn.
|
||||
/// </value>
|
||||
public string DN { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the attributes matching the specified attrName.
|
||||
/// </summary>
|
||||
/// <param name="attrName">The name of the attribute or attributes to return.</param>
|
||||
/// <returns>
|
||||
/// The attribute matching the name.
|
||||
/// </returns>
|
||||
public LdapAttribute GetAttribute(string attrName) => _attrs[attrName];
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The attribute set of the entry.
|
||||
/// </returns>
|
||||
public LdapAttributeSet GetAttributeSet() => _attrs;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="subtype">One or more subtype specification(s), separated
|
||||
/// with semicolons. The "lang-ja" and
|
||||
/// "lang-en;phonetic" are valid subtype
|
||||
/// specifications.</param>
|
||||
/// <returns>
|
||||
/// An attribute set from the entry with the attributes that
|
||||
/// match the specified subtypes or an empty set if no attributes
|
||||
/// match.
|
||||
/// </returns>
|
||||
public LdapAttributeSet GetAttributeSet(string subtype) => _attrs.GetSubset(subtype);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LdapAttribute"/> class.
|
||||
/// Constructs an attribute with no values.
|
||||
/// </summary>
|
||||
/// <param name="attrName">Name of the attribute.</param>
|
||||
/// <exception cref="ArgumentException">Attribute name cannot be null.</exception>
|
||||
public LdapAttribute(string attrName)
|
||||
{
|
||||
Name = attrName ?? throw new ArgumentNullException(nameof(attrName));
|
||||
_baseName = GetBaseName(attrName);
|
||||
_subTypes = GetSubtypes(attrName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LdapAttribute" /> class.
|
||||
/// Constructs an attribute with a single <see cref="System.String" /> value.
|
||||
/// </summary>
|
||||
/// <param name="attrName">Name of the attribute.</param>
|
||||
/// <param name="attrString">Value of the attribute as a string.</param>
|
||||
/// <exception cref="ArgumentException">Attribute value cannot be null.</exception>
|
||||
public LdapAttribute(string attrName, string attrString)
|
||||
: this(attrName)
|
||||
{
|
||||
Add(Encoding.UTF8.GetSBytes(attrString));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the values of the attribute as an array of bytes.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The byte value array.
|
||||
/// </value>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the values of the attribute as an array of strings.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The string value array.
|
||||
/// </value>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the the first value of the attribute as an UTF-8 string.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The string value.
|
||||
/// </value>
|
||||
public string StringValue => _values == null ? null : Encoding.UTF8.GetString((sbyte[])_values[0]);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the first value of the attribute as a byte array or null.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The byte value.
|
||||
/// </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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The language subtype.
|
||||
/// </value>
|
||||
public string LangSubtype => _subTypes?.FirstOrDefault(t => t.StartsWith("lang-"));
|
||||
|
||||
/// <summary>
|
||||
/// Returns the name of the attribute.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name.
|
||||
/// </value>
|
||||
public string Name { get; }
|
||||
|
||||
internal string Value
|
||||
{
|
||||
set
|
||||
{
|
||||
_values = null;
|
||||
|
||||
Add(Encoding.UTF8.GetSBytes(value));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="attrName">Name of the attribute from which to extract
|
||||
/// the subtypes.</param>
|
||||
/// <returns>
|
||||
/// An array subtypes or null if the attribute has none.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentException">Attribute name cannot be null.</exception>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the base name of the specified attribute name.
|
||||
/// For example, if the attribute name is cn;lang-ja;phonetic,
|
||||
/// this method returns cn.
|
||||
/// </summary>
|
||||
/// <param name="attrName">Name of the attribute from which to extract the
|
||||
/// base name.</param>
|
||||
/// <returns> The base name of the attribute. </returns>
|
||||
/// <exception cref="ArgumentException">Attribute name cannot be null.</exception>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clones this instance.
|
||||
/// </summary>
|
||||
/// <returns>A cloned instance.</returns>
|
||||
public LdapAttribute Clone()
|
||||
{
|
||||
var newObj = MemberwiseClone();
|
||||
if (_values != null)
|
||||
{
|
||||
Array.Copy(_values, 0, ((LdapAttribute)newObj)._values, 0, _values.Length);
|
||||
}
|
||||
|
||||
return (LdapAttribute) newObj;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a <see cref="System.String" /> value to the attribute.
|
||||
/// </summary>
|
||||
/// <param name="attrString">Value of the attribute as a String.</param>
|
||||
/// <exception cref="ArgumentException">Attribute value cannot be null.</exception>
|
||||
public void AddValue(string attrString)
|
||||
{
|
||||
if (attrString == null)
|
||||
{
|
||||
throw new ArgumentException("Attribute value cannot be null");
|
||||
}
|
||||
|
||||
Add(Encoding.UTF8.GetSBytes(attrString));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a byte-formatted value to the attribute.
|
||||
/// </summary>
|
||||
/// <param name="attrBytes">Value of the attribute as raw bytes.
|
||||
/// Note: If attrBytes represents a string it should be UTF-8 encoded.</param>
|
||||
/// <exception cref="ArgumentException">Attribute value cannot be null.</exception>
|
||||
public void AddValue(sbyte[] attrBytes)
|
||||
{
|
||||
if (attrBytes == null)
|
||||
{
|
||||
throw new ArgumentException("Attribute value cannot be null");
|
||||
}
|
||||
|
||||
Add(attrBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="attrString">The base64 value of the attribute as a String.</param>
|
||||
/// <exception cref="ArgumentException">Attribute value cannot be null.</exception>
|
||||
public void AddBase64Value(string attrString)
|
||||
{
|
||||
if (attrString == null)
|
||||
{
|
||||
throw new ArgumentException("Attribute value cannot be null");
|
||||
}
|
||||
|
||||
Add(Convert.FromBase64String(attrString).ToSByteArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="attrString">The base64 value of the attribute as a StringBuffer.</param>
|
||||
/// <param name="start">The start index of base64 encoded part, inclusive.</param>
|
||||
/// <param name="end">The end index of base encoded part, exclusive.</param>
|
||||
/// <exception cref="ArgumentNullException">attrString.</exception>
|
||||
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());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="attrChars">The base64 value of the attribute as an array of
|
||||
/// characters.</param>
|
||||
/// <exception cref="ArgumentNullException">attrChars.</exception>
|
||||
public void AddBase64Value(char[] attrChars)
|
||||
{
|
||||
if (attrChars == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(attrChars));
|
||||
}
|
||||
|
||||
Add(Convert.FromBase64CharArray(attrChars, 0, attrChars.Length).ToSByteArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the base name of the attribute.
|
||||
/// For example, if the attribute name is cn;lang-ja;phonetic,
|
||||
/// this method returns cn.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The base name of the attribute.
|
||||
/// </returns>
|
||||
public string GetBaseName() => _baseName;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// An array subtypes or null if the attribute has none.
|
||||
/// </returns>
|
||||
public string[] GetSubtypes() => _subTypes;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="subtype">
|
||||
/// The single subtype to check for.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// True, if the attribute has the specified subtype;
|
||||
/// false, if it doesn't.
|
||||
/// </returns>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="subtypes">
|
||||
/// An array of subtypes to check for.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// True, if the attribute has all the specified subtypes;
|
||||
/// false, if it doesn't have all the subtypes.
|
||||
/// </returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a string value from the attribute.
|
||||
/// </summary>
|
||||
/// <param name="attrString">Value of the attribute as a string.
|
||||
/// Note: Removing a value which is not present in the attribute has
|
||||
/// no effect.</param>
|
||||
/// <exception cref="ArgumentNullException">attrString.</exception>
|
||||
public void RemoveValue(string attrString)
|
||||
{
|
||||
if (attrString == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(attrString));
|
||||
}
|
||||
|
||||
RemoveValue(Encoding.UTF8.GetSBytes(attrString));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a byte-formatted value from the attribute.
|
||||
/// </summary>
|
||||
/// <param name="attrBytes">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.
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentNullException">attrBytes.</exception>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the number of values in the attribute.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The number of values in the attribute.
|
||||
/// </returns>
|
||||
public int Size() => _values?.Length ?? 0;
|
||||
|
||||
/// <summary>
|
||||
/// Compares this object with the specified object for order.
|
||||
/// Ordering is determined by comparing attribute names using the method Compare() of the String class.
|
||||
/// </summary>
|
||||
/// <param name="attribute">The LdapAttribute to be compared to this object.</param>
|
||||
/// <returns>
|
||||
/// Returns a negative integer, zero, or a positive
|
||||
/// integer as this object is less than, equal to, or greater than the
|
||||
/// specified object.
|
||||
/// </returns>
|
||||
public int CompareTo(object attribute)
|
||||
=> string.Compare(Name, ((LdapAttribute)attribute).Name, StringComparison.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of this LdapAttribute.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// a string representation of this LdapAttribute.
|
||||
/// </returns>
|
||||
/// <exception cref="Exception">NullReferenceException.</exception>
|
||||
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("<binary value, length:" + sval.Length);
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Append(sval);
|
||||
}
|
||||
|
||||
result.Append("'");
|
||||
}
|
||||
|
||||
result.Append("}");
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an object to this object's list of attribute values.
|
||||
/// </summary>
|
||||
/// <param name="bytes">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.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <seealso cref="LdapAttribute"></seealso>
|
||||
/// <seealso cref="LdapEntry"></seealso>
|
||||
public class LdapAttributeSet : Dictionary<string, LdapAttribute>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LdapAttributeSet"/> class.
|
||||
/// </summary>
|
||||
public LdapAttributeSet()
|
||||
: base(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// placeholder
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new attribute set containing only the attributes that have
|
||||
/// the specified subtypes.
|
||||
/// For example, suppose an attribute set contains the following
|
||||
/// attributes:
|
||||
/// <ul><li> cn</li><li> cn;lang-ja</li><li> sn;phonetic;lang-ja</li><li> sn;lang-us</li></ul>
|
||||
/// Calling the <c>getSubset</c> method and passing lang-ja as the
|
||||
/// argument, the method returns an attribute set containing the following
|
||||
/// attributes:.
|
||||
/// <ul><li>cn;lang-ja</li><li>sn;phonetic;lang-ja</li></ul>
|
||||
/// </summary>
|
||||
/// <param name="subtype">Semi-colon delimited list of subtypes to include. For
|
||||
/// example:
|
||||
/// <ul><li> "lang-ja" specifies only Japanese language subtypes</li><li> "binary" specifies only binary subtypes</li><li>
|
||||
/// "binary;lang-ja" specifies only Japanese language subtypes
|
||||
/// which also are binary
|
||||
/// </li></ul>
|
||||
/// Note: Novell eDirectory does not currently support language subtypes.
|
||||
/// It does support the "binary" subtype.</param>
|
||||
/// <returns>
|
||||
/// An attribute set containing the attributes that match the
|
||||
/// specified subtype.
|
||||
/// </returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <c>true</c> if this set contains an attribute of the same name
|
||||
/// as the specified attribute.
|
||||
/// </summary>
|
||||
/// <param name="attr">Object of type <c>LdapAttribute</c>.</param>
|
||||
/// <returns>
|
||||
/// true if this set contains the specified attribute.
|
||||
/// </returns>
|
||||
public bool Contains(object attr) => ContainsKey(((LdapAttribute)attr).Name);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="attr">Object of type <c>LdapAttribute</c>.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the attribute was added.
|
||||
/// </returns>
|
||||
public bool Add(LdapAttribute attr)
|
||||
{
|
||||
var name = attr.Name;
|
||||
|
||||
if (ContainsKey(name))
|
||||
return false;
|
||||
|
||||
this[name] = attr;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the specified object from this set if it is present.
|
||||
/// If the specified object is of type <c>LdapAttribute</c>, 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.
|
||||
/// </summary>
|
||||
/// <param name="entry">The entry.</param>
|
||||
/// <returns>
|
||||
/// true if the object was removed.
|
||||
/// </returns>
|
||||
public bool Remove(LdapAttribute entry) => Remove(entry.Name);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <see cref="System.String" /> that represents this instance.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A <see cref="System.String" /> that represents this instance.
|
||||
/// </returns>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
namespace Unosquare.Swan.Networking.Ldap
|
||||
{
|
||||
/// <summary>
|
||||
/// Ldap Modification Operators.
|
||||
/// </summary>
|
||||
public enum LdapModificationOp
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the listed values to the given attribute, creating
|
||||
/// the attribute if it does not already exist.
|
||||
/// </summary>
|
||||
Add = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
Delete = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
Replace = 2,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LDAP valid scopes.
|
||||
/// </summary>
|
||||
public enum LdapScope
|
||||
{
|
||||
/// <summary>
|
||||
/// Used with search to specify that the scope of entrys to search is to
|
||||
/// search only the base object.
|
||||
/// </summary>
|
||||
ScopeBase = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Used with search to specify that the scope of entrys to search is to
|
||||
/// search only the immediate subordinates of the base object.
|
||||
/// </summary>
|
||||
ScopeOne = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Used with search to specify that the scope of entrys to search is to
|
||||
/// search the base object and all entries within its subtree.
|
||||
/// </summary>
|
||||
ScopeSub = 2,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Substring Operators.
|
||||
/// </summary>
|
||||
internal enum SubstringOp
|
||||
{
|
||||
/// <summary>
|
||||
/// Search Filter Identifier for an INITIAL component of a SUBSTRING.
|
||||
/// Note: An initial SUBSTRING is represented as "value*".
|
||||
/// </summary>
|
||||
Initial = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Search Filter Identifier for an ANY component of a SUBSTRING.
|
||||
/// Note: An ANY SUBSTRING is represented as "*value*".
|
||||
/// </summary>
|
||||
Any = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Search Filter Identifier for a FINAL component of a SUBSTRING.
|
||||
/// Note: A FINAL SUBSTRING is represented as "*value".
|
||||
/// </summary>
|
||||
Final = 2,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filtering Operators.
|
||||
/// </summary>
|
||||
internal enum FilterOp
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifier for AND component.
|
||||
/// </summary>
|
||||
And = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Identifier for OR component.
|
||||
/// </summary>
|
||||
Or = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Identifier for NOT component.
|
||||
/// </summary>
|
||||
Not = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Identifier for EQUALITY_MATCH component.
|
||||
/// </summary>
|
||||
EqualityMatch = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Identifier for SUBSTRINGS component.
|
||||
/// </summary>
|
||||
Substrings = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Identifier for GREATER_OR_EQUAL component.
|
||||
/// </summary>
|
||||
GreaterOrEqual = 5,
|
||||
|
||||
/// <summary>
|
||||
/// Identifier for LESS_OR_EQUAL component.
|
||||
/// </summary>
|
||||
LessOrEqual = 6,
|
||||
|
||||
/// <summary>
|
||||
/// Identifier for PRESENT component.
|
||||
/// </summary>
|
||||
Present = 7,
|
||||
|
||||
/// <summary>
|
||||
/// Identifier for APPROX_MATCH component.
|
||||
/// </summary>
|
||||
ApproxMatch = 8,
|
||||
|
||||
/// <summary>
|
||||
/// Identifier for EXTENSIBLE_MATCH component.
|
||||
/// </summary>
|
||||
ExtensibleMatch = 9,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
namespace Unosquare.Swan.Networking.Ldap
|
||||
{
|
||||
/// <summary>
|
||||
/// The base class for Ldap request and response messages.
|
||||
/// Subclassed by response messages used in asynchronous operations.
|
||||
/// </summary>
|
||||
public class LdapMessage
|
||||
{
|
||||
internal RfcLdapMessage Message;
|
||||
|
||||
private int _imsgNum = -1; // This instance LdapMessage number
|
||||
|
||||
private LdapOperation _messageType = LdapOperation.Unknown;
|
||||
|
||||
private string _stringTag;
|
||||
|
||||
internal LdapMessage()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LdapMessage"/> class.
|
||||
/// Creates an LdapMessage when sending a protocol operation and sends
|
||||
/// some optional controls with the message.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="op">The operation type of message.</param>
|
||||
/// <param name="controls">The controls to use with the operation.</param>
|
||||
/// <seealso cref="Type"></seealso>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LdapMessage"/> class.
|
||||
/// Creates an Rfc 2251 LdapMessage when the libraries receive a response
|
||||
/// from a command.
|
||||
/// </summary>
|
||||
/// <param name="message">A response message.</param>
|
||||
internal LdapMessage(RfcLdapMessage message) => Message = message;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the message ID. The message ID is an integer value
|
||||
/// identifying the Ldap request and its response.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The message identifier.
|
||||
/// </value>
|
||||
public virtual int MessageId
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_imsgNum == -1)
|
||||
{
|
||||
_imsgNum = Message.MessageId;
|
||||
}
|
||||
|
||||
return _imsgNum;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the message is a request or a response.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if request; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the identifier tag for this message.
|
||||
/// An identifier can be associated with a message with the
|
||||
/// <c>setTag</c> method.
|
||||
/// Tags are set by the application and not by the API or the server.
|
||||
/// If a server response <c>isRequest() == false</c> has no tag,
|
||||
/// the tag associated with the corresponding server request is used.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The tag.
|
||||
/// </value>
|
||||
public virtual string Tag
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_stringTag != null)
|
||||
{
|
||||
return _stringTag;
|
||||
}
|
||||
|
||||
return Request ? null : RequestingMessage?._stringTag;
|
||||
}
|
||||
|
||||
set => _stringTag = value;
|
||||
}
|
||||
|
||||
private string Name => Type.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <see cref="System.String" /> that represents this instance.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A <see cref="System.String" /> that represents this instance.
|
||||
/// </returns>
|
||||
public override string ToString() => $"{Name}({MessageId}): {Message}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
namespace Unosquare.Swan.Networking.Ldap
|
||||
{
|
||||
/// <summary>
|
||||
/// 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., <c>LdapModification[]</c>.
|
||||
/// 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.
|
||||
/// <b>Add: </b>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.
|
||||
/// <b>Delete: </b>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.
|
||||
/// <b>Replace: </b>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. <a href="http://www.ietf.org/rfc/rfc2251.txt">rfc2251.txt</a>
|
||||
/// </summary>
|
||||
/// <seealso cref="LdapConnection.Modify"></seealso>
|
||||
/// <seealso cref="LdapAttribute"></seealso>
|
||||
public sealed class LdapModification : LdapMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LdapModification" /> class.
|
||||
/// Specifies a modification to be made to an attribute.
|
||||
/// </summary>
|
||||
/// <param name="op">The op.</param>
|
||||
/// <param name="attr">The attribute to modify.</param>
|
||||
public LdapModification(LdapModificationOp op, LdapAttribute attr)
|
||||
{
|
||||
Op = op;
|
||||
Attribute = attr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LdapModification"/> class.
|
||||
/// </summary>
|
||||
/// <param name="op">The op.</param>
|
||||
/// <param name="attrName">Name of the attribute.</param>
|
||||
/// <param name="attrValue">The attribute value.</param>
|
||||
public LdapModification(LdapModificationOp op, string attrName, string attrValue)
|
||||
: this(op, new LdapAttribute(attrName, attrValue))
|
||||
{
|
||||
// placeholder
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the attribute to modify, with any existing values.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The attribute.
|
||||
/// </value>
|
||||
public LdapAttribute Attribute { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the type of modification specified by this object.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The op.
|
||||
/// </value>
|
||||
public LdapModificationOp Op { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
namespace Unosquare.Swan.Networking.Ldap
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a LDAP Modification Request Message.
|
||||
/// </summary>
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.LdapMessage" />
|
||||
public sealed class LdapModifyRequest : LdapMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LdapModifyRequest"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dn">The dn.</param>
|
||||
/// <param name="modifications">The modifications.</param>
|
||||
/// <param name="control">The control.</param>
|
||||
public LdapModifyRequest(string dn, LdapModification[] modifications, LdapControl[] control)
|
||||
: base(LdapOperation.ModifyRequest, new RfcModifyRequest(dn, EncodeModifications(modifications)), control)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the dn.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The dn.
|
||||
/// </value>
|
||||
public string DN => Asn1Object.RequestDn;
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
namespace Unosquare.Swan.Networking.Ldap
|
||||
{
|
||||
/// <summary>
|
||||
/// LDAP Operation.
|
||||
/// </summary>
|
||||
internal enum LdapOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// The unknown
|
||||
/// </summary>
|
||||
Unknown = -1,
|
||||
|
||||
/// <summary>
|
||||
/// A bind request operation.
|
||||
/// BIND_REQUEST = 0
|
||||
/// </summary>
|
||||
BindRequest = 0,
|
||||
|
||||
/// <summary>
|
||||
/// A bind response operation.
|
||||
/// BIND_RESPONSE = 1
|
||||
/// </summary>
|
||||
BindResponse = 1,
|
||||
|
||||
/// <summary>
|
||||
/// An unbind request operation.
|
||||
/// UNBIND_REQUEST = 2
|
||||
/// </summary>
|
||||
UnbindRequest = 2,
|
||||
|
||||
/// <summary>
|
||||
/// A search request operation.
|
||||
/// SEARCH_REQUEST = 3
|
||||
/// </summary>
|
||||
SearchRequest = 3,
|
||||
|
||||
/// <summary>
|
||||
/// A search response containing data.
|
||||
/// SEARCH_RESPONSE = 4
|
||||
/// </summary>
|
||||
SearchResponse = 4,
|
||||
|
||||
/// <summary>
|
||||
/// A search result message - contains search status.
|
||||
/// SEARCH_RESULT = 5
|
||||
/// </summary>
|
||||
SearchResult = 5,
|
||||
|
||||
/// <summary>
|
||||
/// A modify request operation.
|
||||
/// MODIFY_REQUEST = 6
|
||||
/// </summary>
|
||||
ModifyRequest = 6,
|
||||
|
||||
/// <summary>
|
||||
/// A modify response operation.
|
||||
/// MODIFY_RESPONSE = 7
|
||||
/// </summary>
|
||||
ModifyResponse = 7,
|
||||
|
||||
/// <summary>
|
||||
/// An abandon request operation.
|
||||
/// ABANDON_REQUEST = 16
|
||||
/// </summary>
|
||||
AbandonRequest = 16,
|
||||
|
||||
/// <summary>
|
||||
/// A search result reference operation.
|
||||
/// SEARCH_RESULT_REFERENCE = 19
|
||||
/// </summary>
|
||||
SearchResultReference = 19,
|
||||
|
||||
/// <summary>
|
||||
/// An extended request operation.
|
||||
/// EXTENDED_REQUEST = 23
|
||||
/// </summary>
|
||||
ExtendedRequest = 23,
|
||||
|
||||
/// <summary>
|
||||
/// An extended response operation.
|
||||
/// EXTENDED_RESPONSE = 24
|
||||
/// </summary>
|
||||
ExtendedResponse = 24,
|
||||
|
||||
/// <summary>
|
||||
/// An intermediate response operation.
|
||||
/// INTERMEDIATE_RESPONSE = 25
|
||||
/// </summary>
|
||||
IntermediateResponse = 25,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ASN1 tags.
|
||||
/// </summary>
|
||||
internal enum Asn1IdentifierTag
|
||||
{
|
||||
/// <summary>
|
||||
/// Universal tag class.
|
||||
/// </summary>
|
||||
Universal = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Application-wide tag class.
|
||||
/// </summary>
|
||||
Application = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Context-specific tag class.
|
||||
/// </summary>
|
||||
Context = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Private-use tag class.
|
||||
/// </summary>
|
||||
Private = 3,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
namespace Unosquare.Swan.Networking.Ldap
|
||||
{
|
||||
using System.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an Ldap Search request.
|
||||
/// </summary>
|
||||
/// <seealso cref="LdapMessage" />
|
||||
internal sealed class LdapSearchRequest : LdapMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LdapSearchRequest"/> class.
|
||||
/// </summary>
|
||||
/// <param name="ldapBase">The base distinguished name to search from.</param>
|
||||
/// <param name="scope">The scope of the entries to search. The following
|
||||
/// are the valid options:.
|
||||
/// <ul><li>SCOPE_BASE - searches only the base DN</li><li>SCOPE_ONE - searches only entries under the base DN</li><li>
|
||||
/// SCOPE_SUB - searches the base DN and all entries
|
||||
/// within its subtree
|
||||
/// </li></ul></param>
|
||||
/// <param name="filter">The search filter specifying the search criteria.</param>
|
||||
/// <param name="attrs">The names of attributes to retrieve.
|
||||
/// operation exceeds the time limit.</param>
|
||||
/// <param name="dereference">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.</param>
|
||||
/// <param name="maxResults">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.</param>
|
||||
/// <param name="serverTimeLimit">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.</param>
|
||||
/// <param name="typesOnly">If true, returns the names but not the values of
|
||||
/// the attributes found. If false, returns the
|
||||
/// names and values for attributes found.</param>
|
||||
/// <param name="cont">Any controls that apply to the search request.
|
||||
/// or null if none.</param>
|
||||
/// <seealso cref="LdapConnection.Search"></seealso>
|
||||
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)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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:.
|
||||
/// <ul><li>AND - followed by an Iterator value</li><li>OR - followed by an Iterator value</li><li>NOT - followed by an Iterator value</li><li>
|
||||
/// EQUALITY_MATCH - followed by the attribute name represented as a
|
||||
/// String, and by the attribute value represented as a byte array
|
||||
/// </li><li>
|
||||
/// GREATER_OR_EQUAL - followed by the attribute name represented as a
|
||||
/// String, and by the attribute value represented as a byte array
|
||||
/// </li><li>
|
||||
/// LESS_OR_EQUAL - followed by the attribute name represented as a
|
||||
/// String, and by the attribute value represented as a byte array
|
||||
/// </li><li>
|
||||
/// APPROX_MATCH - followed by the attribute name represented as a
|
||||
/// String, and by the attribute value represented as a byte array
|
||||
/// </li><li>PRESENT - followed by a attribute name respresented as a String</li><li>
|
||||
/// 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.
|
||||
/// </li><li>
|
||||
/// 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.
|
||||
/// </li></ul>
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The search filter.
|
||||
/// </value>
|
||||
public IEnumerator SearchFilter => RfcFilter.GetFilterIterator();
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the Base DN for a search request.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// the base DN for a search request.
|
||||
/// </returns>
|
||||
public string DN => Asn1Object.RequestDn;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the scope of a search request.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The scope.
|
||||
/// </value>
|
||||
public int Scope => ((Asn1Enumerated)((RfcSearchRequest)Asn1Object.Get(1)).Get(1)).IntValue();
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the behaviour of dereferencing aliases on a search request.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The dereference.
|
||||
/// </value>
|
||||
public int Dereference => ((Asn1Enumerated)((RfcSearchRequest)Asn1Object.Get(1)).Get(2)).IntValue();
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the maximum number of entries to be returned on a search.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The maximum results.
|
||||
/// </value>
|
||||
public int MaxResults => ((Asn1Integer)((RfcSearchRequest)Asn1Object.Get(1)).Get(3)).IntValue();
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the server time limit for a search request.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The server time limit.
|
||||
/// </value>
|
||||
public int ServerTimeLimit => ((Asn1Integer)((RfcSearchRequest)Asn1Object.Get(1)).Get(4)).IntValue();
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves whether attribute values or only attribute types(names) should
|
||||
/// be returned in a search request.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if [types only]; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool TypesOnly => ((Asn1Boolean)((RfcSearchRequest)Asn1Object.Get(1)).Get(5)).BooleanValue();
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an array of attribute names to request for in a search.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The attributes.
|
||||
/// </value>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a string representation of the filter in this search request.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The string filter.
|
||||
/// </value>
|
||||
public string StringFilter => RfcFilter.FilterToString();
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an SearchFilter object representing a filter for a search request.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The RFC filter.
|
||||
/// </value>
|
||||
private RfcFilter RfcFilter => (RfcFilter)((RfcSearchRequest)Asn1Object.Get(1)).Get(6);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
namespace Unosquare.Swan.Networking.Ldap
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
/// <summary>
|
||||
/// An LdapSearchResults object is returned from a synchronous search
|
||||
/// operation. It provides access to all results received during the
|
||||
/// operation (entries and exceptions).
|
||||
/// </summary>
|
||||
/// <seealso cref="LdapConnection.Search"></seealso>
|
||||
public sealed class LdapSearchResults
|
||||
{
|
||||
private readonly List<RfcLdapMessage> _messages;
|
||||
private readonly int _messageId;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LdapSearchResults" /> class.
|
||||
/// </summary>
|
||||
/// <param name="messages">The messages.</param>
|
||||
/// <param name="messageId">The message identifier.</param>
|
||||
internal LdapSearchResults(List<RfcLdapMessage> messages, int messageId)
|
||||
{
|
||||
_messages = messages;
|
||||
_messageId = messageId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The count.
|
||||
/// </value>
|
||||
public int Count => new List<RfcLdapMessage>(_messages)
|
||||
.Count(x => x.MessageId == _messageId && GetResponse(x) is LdapSearchResult);
|
||||
|
||||
/// <summary>
|
||||
/// Reports if there are more search results.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// true if there are more search results.
|
||||
/// </returns>
|
||||
public bool HasMore() => new List<RfcLdapMessage>(_messages)
|
||||
.Any(x => x.MessageId == _messageId && GetResponse(x) is LdapSearchResult);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The next search result as an LdapEntry.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Next - No more results.</exception>
|
||||
public LdapEntry Next()
|
||||
{
|
||||
var list = new List<RfcLdapMessage>(_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
namespace Unosquare.Swan.Networking.Ldap
|
||||
{
|
||||
/// <summary>
|
||||
/// LDAP Connection Status Code.
|
||||
/// </summary>
|
||||
public enum LdapStatusCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates the requested client operation completed successfully.
|
||||
/// SUCCESS = 0<p />
|
||||
/// </summary>
|
||||
Success = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
OperationsError = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the server has received an invalid or malformed request
|
||||
/// from the client.
|
||||
/// PROTOCOL_ERROR = 2
|
||||
/// </summary>
|
||||
ProtocolError = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
TimeLimitExceeded = 3,
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
SizeLimitExceeded = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Does not indicate an error condition. Indicates that the results of
|
||||
/// a compare operation are false.
|
||||
/// COMPARE_FALSE = 5
|
||||
/// </summary>
|
||||
CompareFalse = 5,
|
||||
|
||||
/// <summary>
|
||||
/// Does not indicate an error condition. Indicates that the results of a
|
||||
/// compare operation are true.
|
||||
/// COMPARE_TRUE = 6
|
||||
/// </summary>
|
||||
CompareTrue = 6,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that during a bind operation the client requested an
|
||||
/// authentication method not supported by the Ldap server.
|
||||
/// AUTH_METHOD_NOT_SUPPORTED = 7
|
||||
/// </summary>
|
||||
AuthMethodNotSupported = 7,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates a problem with the level of authentication.
|
||||
/// One of the following has occurred:
|
||||
/// <ul><li>
|
||||
/// In bind requests, the Ldap server accepts only strong
|
||||
/// authentication.
|
||||
/// </li><li>
|
||||
/// In a client request, the client requested an operation such as delete
|
||||
/// that requires strong authentication.
|
||||
/// </li><li>
|
||||
/// 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.
|
||||
/// </li></ul>
|
||||
/// STRONG_AUTH_REQUIRED = 8
|
||||
/// </summary>
|
||||
StrongAuthRequired = 8,
|
||||
|
||||
/// <summary>
|
||||
/// Returned by some Ldap servers to Ldapv2 clients to indicate that a referral
|
||||
/// has been returned in the error string.
|
||||
/// Ldap_PARTIAL_RESULTS = 9
|
||||
/// </summary>
|
||||
LdapPartialResults = 9,
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
Referral = 10,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that an Ldap server limit set by an administrative authority
|
||||
/// has been exceeded.
|
||||
/// ADMIN_LIMIT_EXCEEDED = 11
|
||||
/// </summary>
|
||||
AdminLimitExceeded = 11,
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
UnavailableCriticalExtension = 12,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the session is not protected by a protocol such as
|
||||
/// Transport Layer Security (TLS), which provides session confidentiality.
|
||||
/// CONFIDENTIALITY_REQUIRED = 13
|
||||
/// </summary>
|
||||
ConfidentialityRequired = 13,
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
SaslBindInProgress = 14,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the attribute specified in the modify or compare
|
||||
/// operation does not exist in the entry.
|
||||
/// NO_SUCH_ATTRIBUTE = 16
|
||||
/// </summary>
|
||||
NoSuchAttribute = 16,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the attribute specified in the modify or add operation
|
||||
/// does not exist in the Ldap server's schema.
|
||||
/// UNDEFINED_ATTRIBUTE_TYPE = 17
|
||||
/// </summary>
|
||||
UndefinedAttributeType = 17,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the matching rule specified in the search filter does
|
||||
/// not match a rule defined for the attribute's syntax.
|
||||
/// INAPPROPRIATE_MATCHING = 18
|
||||
/// </summary>
|
||||
InappropriateMatching = 18,
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
ConstraintViolation = 19,
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
AttributeOrValueExists = 20,
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
InvalidAttributeSyntax = 21,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates the target object cannot be found.
|
||||
/// This code is not returned on the following operations:
|
||||
/// <ul>
|
||||
/// <li>
|
||||
/// Search operations that find the search base but cannot find any
|
||||
/// entries that match the search filter.
|
||||
/// </li>
|
||||
/// <li>Bind operations.</li>
|
||||
/// </ul>
|
||||
/// NO_SUCH_OBJECT = 32
|
||||
/// </summary>
|
||||
NoSuchObject = 32,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that an error occurred when an alias was dereferenced.
|
||||
/// ALIAS_PROBLEM = 33
|
||||
/// </summary>
|
||||
AliasProblem = 33,
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
InvalidDnSyntax = 34,
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
IsLeaf = 35,
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
AliasDereferencingProblem = 36,
|
||||
|
||||
/// <summary>
|
||||
/// 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:
|
||||
/// <ul>
|
||||
/// <li>
|
||||
/// The client returns simple credentials when strong credentials are
|
||||
/// required.
|
||||
/// </li>
|
||||
/// <li>
|
||||
/// The client returns a DN and a password for a simple bind when the
|
||||
/// entry does not have a password defined.
|
||||
/// </li>
|
||||
/// </ul>
|
||||
/// INAPPROPRIATE_AUTHENTICATION = 48
|
||||
/// </summary>
|
||||
InappropriateAuthentication = 48,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that invalid information was passed during a bind operation.
|
||||
/// One of the following occurred:
|
||||
/// <ul>
|
||||
/// <li> The client passed either an incorrect DN or password.</li>
|
||||
/// <li>
|
||||
/// The password is incorrect because it has expired, intruder detection
|
||||
/// has locked the account, or some other similar reason.
|
||||
/// </li>
|
||||
/// </ul>
|
||||
/// INVALID_CREDENTIALS = 49
|
||||
/// </summary>
|
||||
InvalidCredentials = 49,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the caller does not have sufficient rights to perform
|
||||
/// the requested operation.
|
||||
/// INSUFFICIENT_ACCESS_RIGHTS = 50
|
||||
/// </summary>
|
||||
InsufficientAccessRights = 50,
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
Busy = 51,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the Ldap server cannot process the client's bind
|
||||
/// request, usually because it is shutting down.
|
||||
/// UNAVAILABLE = 52
|
||||
/// </summary>
|
||||
Unavailable = 52,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the Ldap server cannot process the request because of
|
||||
/// server-defined restrictions.
|
||||
/// This error is returned for the following reasons:
|
||||
/// <ul>
|
||||
/// <li>The add entry request violates the server's structure rules.</li>
|
||||
/// <li>
|
||||
/// The modify attribute request specifies attributes that users
|
||||
/// cannot modify.
|
||||
/// </li>
|
||||
/// </ul>
|
||||
/// UNWILLING_TO_PERFORM = 53
|
||||
/// </summary>
|
||||
UnwillingToPerform = 53,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the client discovered an alias or referral loop,
|
||||
/// and is thus unable to complete this request.
|
||||
/// LOOP_DETECT = 54
|
||||
/// </summary>
|
||||
LoopDetect = 54,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the add or modify DN operation violates the schema's
|
||||
/// structure rules.
|
||||
/// For example,
|
||||
/// <ul>
|
||||
/// <li>The request places the entry subordinate to an alias.</li>
|
||||
/// <li>
|
||||
/// The request places the entry subordinate to a container that
|
||||
/// is forbidden by the containment rules.
|
||||
/// </li>
|
||||
/// <li>The RDN for the entry uses a forbidden attribute type.</li>
|
||||
/// </ul>
|
||||
/// NAMING_VIOLATION = 64
|
||||
/// </summary>
|
||||
NamingViolation = 64,
|
||||
|
||||
/// <summary>
|
||||
/// 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:
|
||||
/// <ul>
|
||||
/// <li>
|
||||
/// The add or modify operation tries to add an entry without a value
|
||||
/// for a required attribute.
|
||||
/// </li>
|
||||
/// <li>
|
||||
/// The add or modify operation tries to add an entry with a value for
|
||||
/// an attribute which the class definition does not contain.
|
||||
/// </li>
|
||||
/// <li>
|
||||
/// The modify operation tries to remove a required attribute without
|
||||
/// removing the auxiliary class that defines the attribute as required.
|
||||
/// </li>
|
||||
/// </ul>
|
||||
/// OBJECT_CLASS_VIOLATION = 65
|
||||
/// </summary>
|
||||
ObjectClassViolation = 65,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the requested operation is permitted only on leaf entries.
|
||||
/// For example, the following types of requests return this error:
|
||||
/// <ul>
|
||||
/// <li>The client requests a delete operation on a parent entry.</li>
|
||||
/// <li> The client request a modify DN operation on a parent entry.</li>
|
||||
/// </ul>
|
||||
/// NOT_ALLOWED_ON_NONLEAF = 66
|
||||
/// </summary>
|
||||
NotAllowedOnNonleaf = 66,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the modify operation attempted to remove an attribute
|
||||
/// value that forms the entry's relative distinguished name.
|
||||
/// NOT_ALLOWED_ON_RDN = 67
|
||||
/// </summary>
|
||||
NotAllowedOnRdn = 67,
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
EntryAlreadyExists = 68,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the modify operation attempted to modify the structure
|
||||
/// rules of an object class.
|
||||
/// OBJECT_CLASS_MODS_PROHIBITED = 69
|
||||
/// </summary>
|
||||
ObjectClassModsProhibited = 69,
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
AffectsMultipleDsas = 71,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates an unknown error condition.
|
||||
/// OTHER = 80
|
||||
/// </summary>
|
||||
Other = 80,
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Local Errors, resulting from actions other than an operation on a server
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
ServerDown = 81,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the Ldap client has an error. This is usually a failed
|
||||
/// dynamic memory allocation error.
|
||||
/// LOCAL_ERROR = 82
|
||||
/// </summary>
|
||||
LocalError = 82,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the Ldap client encountered errors when encoding an
|
||||
/// Ldap request intended for the Ldap server.
|
||||
/// ENCODING_ERROR = 83
|
||||
/// </summary>
|
||||
EncodingError = 83,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the Ldap client encountered errors when decoding an
|
||||
/// Ldap response from the Ldap server.
|
||||
/// DECODING_ERROR = 84
|
||||
/// </summary>
|
||||
DecodingError = 84,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the time limit of the Ldap client was exceeded while
|
||||
/// waiting for a result.
|
||||
/// Ldap_TIMEOUT = 85
|
||||
/// </summary>
|
||||
LdapTimeout = 85,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that a bind method was called with an unknown
|
||||
/// authentication method.
|
||||
/// AUTH_UNKNOWN = 86
|
||||
/// </summary>
|
||||
AuthUnknown = 86,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the search method was called with an invalid
|
||||
/// search filter.
|
||||
/// FILTER_ERROR = 87
|
||||
/// </summary>
|
||||
FilterError = 87,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the user cancelled the Ldap operation.
|
||||
/// USER_CANCELLED = 88
|
||||
/// </summary>
|
||||
UserCancelled = 88,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that a dynamic memory allocation method failed when calling
|
||||
/// an Ldap method.
|
||||
/// NO_MEMORY = 90
|
||||
/// </summary>
|
||||
NoMemory = 90,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the Ldap client has lost either its connection or
|
||||
/// cannot establish a connection to the Ldap server.
|
||||
/// CONNECT_ERROR = 91
|
||||
/// </summary>
|
||||
ConnectError = 91,
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
LdapNotSupported = 92,
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
ControlNotFound = 93,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the Ldap server sent no results.
|
||||
/// NO_RESULTS_RETURNED = 94
|
||||
/// </summary>
|
||||
NoResultsReturned = 94,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that more results are chained in the result message.
|
||||
/// MORE_RESULTS_TO_RETURN = 95
|
||||
/// </summary>
|
||||
MoreResultsToReturn = 95,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates the Ldap libraries detected a loop. Usually this happens
|
||||
/// when following referrals.
|
||||
/// CLIENT_LOOP = 96
|
||||
/// </summary>
|
||||
ClientLoop = 96,
|
||||
|
||||
/// <summary>
|
||||
/// 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:
|
||||
/// <ul>
|
||||
/// <li>Suppose the hop limit is two.</li>
|
||||
/// <li>
|
||||
/// 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).
|
||||
/// </li>
|
||||
/// </ul>
|
||||
/// With these conditions, the hop limit is exceeded and the Ldap
|
||||
/// libraries set this code.
|
||||
/// REFERRAL_LIMIT_EXCEEDED = 97
|
||||
/// </summary>
|
||||
ReferralLimitExceeded = 97,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the server response to a request is invalid.
|
||||
/// INVALID_RESPONSE = 100
|
||||
/// </summary>
|
||||
InvalidResponse = 100,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the server response to a request is ambiguous.
|
||||
/// AMBIGUOUS_RESPONSE = 101
|
||||
/// </summary>
|
||||
AmbiguousResponse = 101,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that TLS is not supported on the server.
|
||||
/// TLS_NOT_SUPPORTED = 112
|
||||
/// </summary>
|
||||
TlsNotSupported = 112,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that SSL Handshake could not succeed.
|
||||
/// SSL_HANDSHAKE_FAILED = 113
|
||||
/// </summary>
|
||||
SslHandshakeFailed = 113,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that SSL Provider could not be found.
|
||||
/// SSL_PROVIDER_NOT_FOUND = 114
|
||||
/// </summary>
|
||||
SslProviderNotFound = 114,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
namespace Unosquare.Swan.Networking.Ldap
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// The class performs token processing from strings.
|
||||
/// </summary>
|
||||
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<string> _elements;
|
||||
|
||||
private string _source;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Tokenizer" /> class.
|
||||
/// Initializes a new class instance with a specified string to process
|
||||
/// and the specified token delimiters to use.
|
||||
/// </summary>
|
||||
/// <param name="source">String to tokenize.</param>
|
||||
/// <param name="delimiters">String containing the delimiters.</param>
|
||||
/// <param name="retDel">if set to <c>true</c> [ret delete].</param>
|
||||
public Tokenizer(string source, string delimiters, bool retDel = false)
|
||||
{
|
||||
_elements = new List<string>();
|
||||
_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<string>();
|
||||
_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an Ldap Matching Rule Assertion.
|
||||
/// <pre>
|
||||
/// MatchingRuleAssertion ::= SEQUENCE {
|
||||
/// matchingRule [1] MatchingRuleId OPTIONAL,
|
||||
/// type [2] AttributeDescription OPTIONAL,
|
||||
/// matchValue [3] AssertionValue,
|
||||
/// dnAttributes [4] BOOLEAN DEFAULT FALSE }
|
||||
/// </pre></summary>
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The AttributeDescriptionList is used to list attributes to be returned in
|
||||
/// a search request.
|
||||
/// <pre>
|
||||
/// AttributeDescriptionList ::= SEQUENCE OF
|
||||
/// AttributeDescription
|
||||
/// </pre></summary>
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1SequenceOf" />
|
||||
internal class RfcAttributeDescriptionList : Asn1SequenceOf
|
||||
{
|
||||
public RfcAttributeDescriptionList(string[] attrs)
|
||||
: base(attrs?.Length ?? 0)
|
||||
{
|
||||
if (attrs == null) return;
|
||||
|
||||
foreach (var attr in attrs)
|
||||
{
|
||||
Add(attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an Ldap Search Request.
|
||||
/// <pre>
|
||||
/// 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 }
|
||||
/// </pre>
|
||||
/// </summary>
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.IRfcRequest" />
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an Ldap Substring Filter.
|
||||
/// <pre>
|
||||
/// SubstringFilter ::= SEQUENCE {
|
||||
/// type AttributeDescription,
|
||||
/// -- at least one must be present
|
||||
/// substrings SEQUENCE OF CHOICE {
|
||||
/// initial [0] LdapString,
|
||||
/// any [1] LdapString,
|
||||
/// final [2] LdapString } }
|
||||
/// </pre>
|
||||
/// </summary>
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
|
||||
internal class RfcSubstringFilter : Asn1Sequence
|
||||
{
|
||||
public RfcSubstringFilter(string type, Asn1Object substrings)
|
||||
: base(2)
|
||||
{
|
||||
Add(type);
|
||||
Add(substrings);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an Ldap Attribute Value Assertion.
|
||||
/// <pre>
|
||||
/// AttributeValueAssertion ::= SEQUENCE {
|
||||
/// attributeDesc AttributeDescription,
|
||||
/// assertionValue AssertionValue }
|
||||
/// </pre>
|
||||
/// </summary>
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary> Encapsulates an Ldap Bind properties.</summary>
|
||||
internal class BindProperties
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BindProperties" /> class.
|
||||
/// </summary>
|
||||
/// <param name="version">The version.</param>
|
||||
/// <param name="dn">The dn.</param>
|
||||
/// <param name="method">The method.</param>
|
||||
/// <param name="anonymous">if set to <c>true</c> [anonymous].</param>
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
namespace Unosquare.Swan.Networking.Ldap
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an Ldap Control.
|
||||
/// <pre>
|
||||
/// Control ::= SEQUENCE {
|
||||
/// controlType LdapOID,
|
||||
/// criticality BOOLEAN DEFAULT FALSE,
|
||||
/// controlValue OCTET STRING OPTIONAL }
|
||||
/// </pre>
|
||||
/// </summary>
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
|
||||
internal class RfcControl : Asn1Sequence
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RfcControl"/> 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.
|
||||
/// </summary>
|
||||
/// <param name="controlType">Type of the control.</param>
|
||||
/// <param name="criticality">The criticality.</param>
|
||||
/// <param name="controlValue">The control value.</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents Ldap Sasl Credentials.
|
||||
/// <pre>
|
||||
/// SaslCredentials ::= SEQUENCE {
|
||||
/// mechanism LdapString,
|
||||
/// credentials OCTET STRING OPTIONAL }
|
||||
/// </pre></summary>
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
|
||||
internal class RfcSaslCredentials : Asn1Sequence
|
||||
{
|
||||
public RfcSaslCredentials(string mechanism, sbyte[] credentials = null)
|
||||
: base(2)
|
||||
{
|
||||
Add(mechanism);
|
||||
if (credentials != null)
|
||||
Add(new Asn1OctetString(credentials));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an Ldap Authentication Choice.
|
||||
/// <pre>
|
||||
/// AuthenticationChoice ::= CHOICE {
|
||||
/// simple [0] OCTET STRING,
|
||||
/// -- 1 and 2 reserved
|
||||
/// sasl [3] SaslCredentials }
|
||||
/// </pre></summary>
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Choice" />
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,246 @@
|
||||
namespace Unosquare.Swan.Networking.Ldap
|
||||
{
|
||||
using System.IO;
|
||||
|
||||
/// <summary>
|
||||
/// Encapsulates a single search result that is in response to an asynchronous
|
||||
/// search operation.
|
||||
/// </summary>
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.LdapMessage" />
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an Ldap Search Result Reference.
|
||||
/// <pre>
|
||||
/// SearchResultReference ::= [APPLICATION 19] SEQUENCE OF LdapURL
|
||||
/// </pre>
|
||||
/// </summary>
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1SequenceOf" />
|
||||
internal class RfcSearchResultReference : Asn1SequenceOf
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RfcSearchResultReference"/> class.
|
||||
/// The only time a client will create a SearchResultReference is when it is
|
||||
/// decoding it from an Stream.
|
||||
/// </summary>
|
||||
/// <param name="stream">The streab.</param>
|
||||
/// <param name="len">The length.</param>
|
||||
public RfcSearchResultReference(Stream stream, int len)
|
||||
: base(stream, len)
|
||||
{
|
||||
}
|
||||
|
||||
public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.SearchResultReference);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an Ldap Extended Response.
|
||||
/// <pre>
|
||||
/// ExtendedResponse ::= [APPLICATION 24] SEQUENCE {
|
||||
/// COMPONENTS OF LdapResult,
|
||||
/// responseName [10] LdapOID OPTIONAL,
|
||||
/// response [11] OCTET STRING OPTIONAL }
|
||||
/// </pre>
|
||||
/// </summary>
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.IRfcResponse" />
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RfcExtendedResponse"/> class.
|
||||
/// The only time a client will create a ExtendedResponse is when it is
|
||||
/// decoding it from an stream.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream.</param>
|
||||
/// <param name="len">The length.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents and Ldap Bind Response.
|
||||
/// <pre>
|
||||
/// BindResponse ::= [APPLICATION 1] SEQUENCE {
|
||||
/// COMPONENTS OF LdapResult,
|
||||
/// serverSaslCreds [7] OCTET STRING OPTIONAL }
|
||||
/// </pre>
|
||||
/// </summary>
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.IRfcResponse" />
|
||||
internal class RfcBindResponse : Asn1Sequence, IRfcResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RfcBindResponse"/> 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.
|
||||
/// </summary>
|
||||
/// <param name="stream">The in renamed.</param>
|
||||
/// <param name="len">The length.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 }.
|
||||
/// </summary>
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.IRfcResponse" />
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
namespace Unosquare.Swan.Networking.Ldap
|
||||
{
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an Ldap Message.
|
||||
/// <pre>
|
||||
/// 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 }
|
||||
/// </pre>
|
||||
/// 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.).
|
||||
/// </summary>
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
|
||||
internal sealed class RfcLdapMessage : Asn1Sequence
|
||||
{
|
||||
private readonly Asn1Object _op;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RfcLdapMessage"/> class.
|
||||
/// Create an RfcLdapMessage request from input parameters.
|
||||
/// </summary>
|
||||
/// <param name="op">The op.</param>
|
||||
/// <param name="controls">The controls.</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RfcLdapMessage"/> class.
|
||||
/// Will decode an RfcLdapMessage directly from an InputStream.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream.</param>
|
||||
/// <param name="len">The length.</param>
|
||||
/// <exception cref="Exception">RfcLdapMessage: Invalid tag: " + protocolOpId.Tag.</exception>
|
||||
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();
|
||||
|
||||
/// <summary> Returns this RfcLdapMessage's message type.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents Ldap Controls.
|
||||
/// <pre>
|
||||
/// Controls ::= SEQUENCE OF Control
|
||||
/// </pre>
|
||||
/// </summary>
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1SequenceOf" />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal interface IRfcResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the result code.
|
||||
/// </summary>
|
||||
/// <returns>Asn1Enumerated.</returns>
|
||||
Asn1Enumerated GetResultCode();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the matched dn.
|
||||
/// </summary>
|
||||
/// <returns>RfcLdapDN.</returns>
|
||||
Asn1OctetString GetMatchedDN();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the error message.
|
||||
/// </summary>
|
||||
/// <returns>RfcLdapString.</returns>
|
||||
Asn1OctetString GetErrorMessage();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the referral.
|
||||
/// </summary>
|
||||
/// <returns>Asn1SequenceOf.</returns>
|
||||
Asn1SequenceOf GetReferral();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This interface represents Protocol Operations that are requests from a
|
||||
/// client.
|
||||
/// </summary>
|
||||
internal interface IRfcRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds a new request using the data from the this object.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="System.String" />.</returns>
|
||||
string GetRequestDN();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an LdapResult.
|
||||
/// <pre>
|
||||
/// 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 }
|
||||
/// </pre>
|
||||
/// </summary>
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.IRfcResponse" />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an Ldap Search Result Done Response.
|
||||
/// <pre>
|
||||
/// SearchResultDone ::= [APPLICATION 5] LdapResult
|
||||
/// </pre>
|
||||
/// </summary>
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.RfcLdapResult" />
|
||||
internal class RfcSearchResultDone : RfcLdapResult
|
||||
{
|
||||
public RfcSearchResultDone(Stream stream, int len)
|
||||
: base(stream, len)
|
||||
{
|
||||
}
|
||||
|
||||
public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.SearchResult);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an Ldap Search Result Entry.
|
||||
/// <pre>
|
||||
/// SearchResultEntry ::= [APPLICATION 4] SEQUENCE {
|
||||
/// objectName LdapDN,
|
||||
/// attributes PartialAttributeList }
|
||||
/// </pre>
|
||||
/// </summary>
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an Ldap Message ID.
|
||||
/// <pre>
|
||||
/// 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.)
|
||||
/// </pre></summary>
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Integer" />
|
||||
internal class RfcMessageID : Asn1Integer
|
||||
{
|
||||
private static int _messageId;
|
||||
private static readonly object SyncRoot = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RfcMessageID"/> 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.
|
||||
/// </summary>
|
||||
protected internal RfcMessageID()
|
||||
: base(MessageId)
|
||||
{
|
||||
}
|
||||
|
||||
private static int MessageId
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
return _messageId < int.MaxValue ? ++_messageId : (_messageId = 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
namespace Unosquare.Swan.Networking.Ldap
|
||||
{
|
||||
using System.IO;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an Ldap Modify Request.
|
||||
/// <pre>
|
||||
/// ModifyRequest ::= [APPLICATION 6] SEQUENCE {
|
||||
/// object LdapDN,
|
||||
/// modification SEQUENCE OF SEQUENCE {
|
||||
/// operation ENUMERATED {
|
||||
/// add (0),
|
||||
/// delete (1),
|
||||
/// replace (2) },
|
||||
/// modification AttributeTypeAndValues } }
|
||||
/// </pre>
|
||||
/// </summary>
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
|
||||
/// <seealso cref="Unosquare.Swan.Networking.Ldap.IRfcRequest" />
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
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;
|
||||
#if !NETSTANDARD1_3
|
||||
using System.Net.Mail;
|
||||
#else
|
||||
using Exceptions;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Represents a basic SMTP client that is capable of submitting messages to an SMTP server.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// The following code explains how to send a simple e-mail.
|
||||
/// <code>
|
||||
/// 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"));
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
///
|
||||
/// The following code demonstrates how to sent an e-mail using a SmtpSessionState:
|
||||
/// <code>
|
||||
/// 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);
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
///
|
||||
/// The following code shows how to send an e-mail with an attachment:
|
||||
/// <code>
|
||||
/// 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);
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class SmtpClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SmtpClient" /> class.
|
||||
/// </summary>
|
||||
/// <param name="host">The host.</param>
|
||||
/// <param name="port">The port.</param>
|
||||
/// <exception cref="ArgumentNullException">host.</exception>
|
||||
public SmtpClient(string host, int port)
|
||||
{
|
||||
Host = host ?? throw new ArgumentNullException(nameof(host));
|
||||
Port = port;
|
||||
ClientHostname = Network.HostName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the credentials. No credentials will be used if set to null.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The credentials.
|
||||
/// </value>
|
||||
public NetworkCredential Credentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the host.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The host.
|
||||
/// </value>
|
||||
public string Host { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the port.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The port.
|
||||
/// </value>
|
||||
public int Port { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the SSL is enabled.
|
||||
/// If set to false, communication between client and server will not be secured.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if [enable SSL]; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool EnableSsl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the client that gets announced to the server.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The client hostname.
|
||||
/// </value>
|
||||
public string ClientHostname { get; set; }
|
||||
|
||||
#if !NETSTANDARD1_3
|
||||
/// <summary>
|
||||
/// Sends an email message asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="sessionId">The session identifier.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <param name="callback">The callback.</param>
|
||||
/// <returns>
|
||||
/// A task that represents the asynchronous of send email operation.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">message.</exception>
|
||||
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);
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="sessionState">The state.</param>
|
||||
/// <param name="sessionId">The session identifier.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <param name="callback">The callback.</param>
|
||||
/// <returns>
|
||||
/// A task that represents the asynchronous of send email operation.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">sessionState.</exception>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="sessionStates">The session states.</param>
|
||||
/// <param name="sessionId">The session identifier.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <param name="callback">The callback.</param>
|
||||
/// <returns>
|
||||
/// A task that represents the asynchronous of send email operation.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">sessionStates.</exception>
|
||||
/// <exception cref="SecurityException">Could not upgrade the channel to SSL.</exception>
|
||||
/// <exception cref="SmtpException">Defines an SMTP Exceptions class.</exception>
|
||||
public async Task SendMailAsync(
|
||||
IEnumerable<SmtpSessionState> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace Unosquare.Swan.Networking
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains useful constants and definitions.
|
||||
/// </summary>
|
||||
public static class SmtpDefinitions
|
||||
{
|
||||
/// <summary>
|
||||
/// The string sequence that delimits the end of the DATA command.
|
||||
/// </summary>
|
||||
public const string SmtpDataCommandTerminator = "\r\n.\r\n";
|
||||
|
||||
/// <summary>
|
||||
/// Lists the AUTH methods supported by default.
|
||||
/// </summary>
|
||||
public static class SmtpAuthMethods
|
||||
{
|
||||
/// <summary>
|
||||
/// The plain method.
|
||||
/// </summary>
|
||||
public const string Plain = "PLAIN";
|
||||
|
||||
/// <summary>
|
||||
/// The login method.
|
||||
/// </summary>
|
||||
public const string Login = "LOGIN";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
namespace Unosquare.Swan.Networking
|
||||
{
|
||||
#if !NETSTANDARD1_3
|
||||
using System.Net.Mail;
|
||||
#else
|
||||
using Exceptions;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Use this class to store the sender session data.
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
namespace Unosquare.Swan.Networking
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an SMTP server response object.
|
||||
/// </summary>
|
||||
public class SmtpServerReply
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SmtpServerReply"/> class.
|
||||
/// </summary>
|
||||
/// <param name="responseCode">The response code.</param>
|
||||
/// <param name="statusCode">The status code.</param>
|
||||
/// <param name="content">The content.</param>
|
||||
public SmtpServerReply(int responseCode, string statusCode, params string[] content)
|
||||
{
|
||||
Content = new List<string>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SmtpServerReply"/> class.
|
||||
/// </summary>
|
||||
public SmtpServerReply()
|
||||
: this(0, string.Empty, string.Empty)
|
||||
{
|
||||
// placeholder
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SmtpServerReply"/> class.
|
||||
/// </summary>
|
||||
/// <param name="responseCode">The response code.</param>
|
||||
/// <param name="statusCode">The status code.</param>
|
||||
/// <param name="content">The content.</param>
|
||||
public SmtpServerReply(int responseCode, string statusCode, string content)
|
||||
: this(responseCode, statusCode, new[] {content})
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SmtpServerReply"/> class.
|
||||
/// </summary>
|
||||
/// <param name="responseCode">The response code.</param>
|
||||
/// <param name="content">The content.</param>
|
||||
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)
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command unrecognized reply.
|
||||
/// </summary>
|
||||
public static SmtpServerReply CommandUnrecognized =>
|
||||
new SmtpServerReply(500, "Syntax error, command unrecognized");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the syntax error arguments reply.
|
||||
/// </summary>
|
||||
public static SmtpServerReply SyntaxErrorArguments =>
|
||||
new SmtpServerReply(501, "Syntax error in parameters or arguments");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command not implemented reply.
|
||||
/// </summary>
|
||||
public static SmtpServerReply CommandNotImplemented => new SmtpServerReply(502, "Command not implemented");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bad sequence of commands reply.
|
||||
/// </summary>
|
||||
public static SmtpServerReply BadSequenceOfCommands => new SmtpServerReply(503, "Bad sequence of commands");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the protocol violation reply.
|
||||
/// </summary>=
|
||||
public static SmtpServerReply ProtocolViolation =>
|
||||
new SmtpServerReply(451, "Requested action aborted: error in processing");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the system status bye reply.
|
||||
/// </summary>
|
||||
public static SmtpServerReply SystemStatusBye =>
|
||||
new SmtpServerReply(221, "Service closing transmission channel");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the system status help reply.
|
||||
/// </summary>=
|
||||
public static SmtpServerReply SystemStatusHelp => new SmtpServerReply(221, "Refer to RFC 5321");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bad syntax command empty reply.
|
||||
/// </summary>
|
||||
public static SmtpServerReply BadSyntaxCommandEmpty => new SmtpServerReply(400, "Error: bad syntax");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the OK reply.
|
||||
/// </summary>
|
||||
public static SmtpServerReply Ok => new SmtpServerReply(250, "OK");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authorization required reply.
|
||||
/// </summary>
|
||||
public static SmtpServerReply AuthorizationRequired => new SmtpServerReply(530, "Authorization Required");
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the response severity.
|
||||
/// </summary>
|
||||
public SmtpReplyCodeSeverities ReplyCodeSeverity { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the response category.
|
||||
/// </summary>
|
||||
public SmtpReplyCodeCategories ReplyCodeCategory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the numeric response code.
|
||||
/// </summary>
|
||||
public int ReplyCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the enhanced status code.
|
||||
/// </summary>
|
||||
public string EnhancedStatusCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the content.
|
||||
/// </summary>
|
||||
public List<string> Content { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the response code is between 200 and 599.
|
||||
/// </summary>
|
||||
public bool IsValid { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is positive.
|
||||
/// </summary>
|
||||
public bool IsPositive => ReplyCode >= 200 && ReplyCode <= 399;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// Parses the specified text into a Server Reply for thorough analysis.
|
||||
/// </summary>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <returns>A new instance of SMTP server response object.</returns>
|
||||
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<string>();
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <see cref="System.String" /> that represents this instance.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A <see cref="System.String" /> that represents this instance.
|
||||
/// </returns>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
namespace Unosquare.Swan.Networking
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the state of an SMTP session associated with a client.
|
||||
/// </summary>
|
||||
public class SmtpSessionState
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SmtpSessionState"/> class.
|
||||
/// </summary>
|
||||
public SmtpSessionState()
|
||||
{
|
||||
DataBuffer = new List<byte>();
|
||||
Reset(true);
|
||||
ResetAuthentication();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the contents of the data buffer.
|
||||
/// </summary>
|
||||
public List<byte> DataBuffer { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance has initiated.
|
||||
/// </summary>
|
||||
public bool HasInitiated { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the current session supports extensions.
|
||||
/// </summary>
|
||||
public bool SupportsExtensions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the client hostname.
|
||||
/// </summary>
|
||||
public string ClientHostname { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the session is currently receiving DATA.
|
||||
/// </summary>
|
||||
public bool IsInDataMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sender address.
|
||||
/// </summary>
|
||||
public string SenderAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the recipients.
|
||||
/// </summary>
|
||||
public List<string> Recipients { get; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the extended data supporting any additional field for storage by a responder implementation.
|
||||
/// </summary>
|
||||
public object ExtendedData { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region AUTH State
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance is in authentication mode.
|
||||
/// </summary>
|
||||
public bool IsInAuthMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the username.
|
||||
/// </summary>
|
||||
public string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password.
|
||||
/// </summary>
|
||||
public string Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance has provided username.
|
||||
/// </summary>
|
||||
public bool HasProvidedUsername => string.IsNullOrWhiteSpace(Username) == false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance is authenticated.
|
||||
/// </summary>
|
||||
public bool IsAuthenticated { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the authentication mode.
|
||||
/// </summary>
|
||||
public string AuthMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance is channel secure.
|
||||
/// </summary>
|
||||
public bool IsChannelSecure { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Resets the authentication state.
|
||||
/// </summary>
|
||||
public void ResetAuthentication()
|
||||
{
|
||||
Username = string.Empty;
|
||||
Password = string.Empty;
|
||||
AuthMode = string.Empty;
|
||||
IsInAuthMode = false;
|
||||
IsAuthenticated = false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// Resets the data mode to false, clears the recipients, the sender address and the data buffer.
|
||||
/// </summary>
|
||||
public void ResetEmail()
|
||||
{
|
||||
IsInDataMode = false;
|
||||
Recipients.Clear();
|
||||
SenderAddress = string.Empty;
|
||||
DataBuffer.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the state table entirely.
|
||||
/// </summary>
|
||||
/// <param name="clearExtensionData">if set to <c>true</c> [clear extension data].</param>
|
||||
public void Reset(bool clearExtensionData)
|
||||
{
|
||||
HasInitiated = false;
|
||||
SupportsExtensions = false;
|
||||
ClientHostname = string.Empty;
|
||||
ResetEmail();
|
||||
|
||||
if (clearExtensionData)
|
||||
ExtendedData = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new object that is a copy of the current instance.
|
||||
/// </summary>
|
||||
/// <returns>A clone.</returns>
|
||||
public virtual SmtpSessionState Clone()
|
||||
{
|
||||
var clonedState = this.CopyPropertiesToNew<SmtpSessionState>(new[] {nameof(DataBuffer)});
|
||||
clonedState.DataBuffer.AddRange(DataBuffer);
|
||||
clonedState.Recipients.AddRange(Recipients);
|
||||
|
||||
return clonedState;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
namespace Unosquare.Swan.Networking
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a little SNMP client based on http://www.java2s.com/Code/CSharp/Network/SimpleSNMP.htm.
|
||||
/// </summary>
|
||||
public static class SnmpClient
|
||||
{
|
||||
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,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Discovers the specified SNMP time out.
|
||||
/// </summary>
|
||||
/// <param name="snmpTimeOut">The SNMP time out.</param>
|
||||
/// <returns>An array of network endpoint as an IP address and a port number.</returns>
|
||||
public static IPEndPoint[] Discover(int snmpTimeOut = 6000)
|
||||
{
|
||||
var endpoints = new List<IPEndPoint>();
|
||||
|
||||
Task[] tasks =
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
using (var udp = new UdpClient(IPAddress.Broadcast.AddressFamily))
|
||||
{
|
||||
udp.EnableBroadcast = true;
|
||||
await udp.SendAsync(
|
||||
DiscoverMessage,
|
||||
DiscoverMessage.Length,
|
||||
new IPEndPoint(IPAddress.Broadcast, 161));
|
||||
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var buffer = new byte[udp.Client.ReceiveBufferSize];
|
||||
EndPoint remote = new IPEndPoint(IPAddress.Any, 0);
|
||||
udp.Client.ReceiveFrom(buffer, ref remote);
|
||||
endpoints.Add(remote as IPEndPoint);
|
||||
}
|
||||
catch
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
#if NET452
|
||||
udp.Close();
|
||||
#endif
|
||||
}
|
||||
}),
|
||||
Task.Delay(snmpTimeOut),
|
||||
};
|
||||
|
||||
Task.WaitAny(tasks);
|
||||
|
||||
return endpoints.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the public.
|
||||
/// </summary>
|
||||
/// <param name="host">The host.</param>
|
||||
/// <returns>
|
||||
/// A string that contains the results of decoding the specified sequence
|
||||
/// of bytes ref=GetString".
|
||||
/// </returns>
|
||||
public static string GetPublicName(IPEndPoint host) => GetString(host, "1.3.6.1.2.1.1.5.0");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the up-time.
|
||||
/// </summary>
|
||||
/// <param name="host">The host.</param>
|
||||
/// <param name="mibString">The mibString.</param>
|
||||
/// <returns>
|
||||
/// A time interval that represents a specified number of seconds,
|
||||
/// where the specification is accurate to the nearest millisecond.
|
||||
/// </returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the string.
|
||||
/// </summary>
|
||||
/// <param name="host">The host.</param>
|
||||
/// <param name="mibString">The mibString.</param>
|
||||
/// <returns>A <see cref="System.String" /> that contains the results of decoding the specified sequence of bytes.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the specified host.
|
||||
/// </summary>
|
||||
/// <param name="host">The host.</param>
|
||||
/// <param name="mibString">The mibString.</param>
|
||||
/// <returns>A byte array containing the results of encoding the specified set of characters.</returns>
|
||||
public static byte[] Get(IPEndPoint host, string mibString) => Get("get", host, "public", mibString);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <param name="host">The host.</param>
|
||||
/// <param name="community">The community.</param>
|
||||
/// <param name="mibString">The mibString.</param>
|
||||
/// <returns>A byte array containing the results of encoding the specified set of characters.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user