Init...
This commit is contained in:
@@ -0,0 +1,886 @@
|
||||
namespace Swan.Net
|
||||
{
|
||||
using Logging;
|
||||
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 Swan.Net;
|
||||
///
|
||||
/// 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 += async (s, e) =>
|
||||
/// {
|
||||
/// // create a new connection
|
||||
/// using (var con = new Connection(e.Client))
|
||||
/// {
|
||||
/// await con.WriteLineAsync("Hello world!");
|
||||
/// }
|
||||
/// };
|
||||
///
|
||||
/// connectionListener.Start();
|
||||
/// Console.ReadLine)=ñ
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// The following code describes how to create a TCP client.
|
||||
/// <code>
|
||||
/// using System.Net.Sockets;
|
||||
/// using System.Text;
|
||||
/// using System.Threading.Tasks;
|
||||
/// using Swan.Net;
|
||||
///
|
||||
/// 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;
|
||||
|
||||
ThreadPool.GetAvailableThreads(out var availableWorkerThreads, out _);
|
||||
ThreadPool.GetMaxThreads(out var maxWorkerThreads, out _);
|
||||
|
||||
var activeThreadPoolTreads = maxWorkerThreads - availableWorkerThreads;
|
||||
|
||||
if (activeThreadPoolTreads < Environment.ProcessorCount / 4)
|
||||
{
|
||||
ThreadPool.QueueUserWorkItem(PerformContinuousReading, this);
|
||||
}
|
||||
else
|
||||
{
|
||||
new Thread(PerformContinuousReading) { IsBackground = true }.Start();
|
||||
}
|
||||
}
|
||||
|
||||
/// <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="cancellationToken">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 cancellationToken = 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, cancellationToken);
|
||||
|
||||
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, cancellationToken).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="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// A byte array containing the results the specified sequence of bytes.
|
||||
/// </returns>
|
||||
public Task<byte[]> ReadDataAsync(CancellationToken cancellationToken = default)
|
||||
=> ReadDataAsync(TimeSpan.FromSeconds(5), cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously reads data as text with the given timeout.
|
||||
/// </summary>
|
||||
/// <param name="timeout">The timeout.</param>
|
||||
/// <param name="cancellationToken">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 cancellationToken = default)
|
||||
{
|
||||
var buffer = await ReadDataAsync(timeout, cancellationToken).ConfigureAwait(false);
|
||||
return buffer == null ? null : TextEncoding.GetString(buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously reads data as text with a 5000 millisecond timeout.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">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 cancellationToken = default)
|
||||
=> ReadTextAsync(TimeSpan.FromSeconds(5), cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Performs the same task as this method's overload but it defaults to a read timeout of 30 seconds.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">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 cancellationToken = default)
|
||||
=> ReadLineAsync(TimeSpan.FromSeconds(30), cancellationToken);
|
||||
|
||||
/// <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="cancellationToken">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 cancellationToken = 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, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (string.IsNullOrEmpty(text))
|
||||
break;
|
||||
|
||||
builder.Append(text);
|
||||
|
||||
if (!text.EndsWith(_newLineSequence)) 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="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous write operation.</returns>
|
||||
public async Task WriteDataAsync(byte[] buffer, bool forceFlush, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
_writeDone.WaitOne();
|
||||
_writeDone.Reset();
|
||||
await ActiveStream.WriteAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
|
||||
if (forceFlush)
|
||||
await ActiveStream.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
DataSentLastTimeUtc = DateTime.UtcNow;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_writeDone.Set();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes text asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous write operation.</returns>
|
||||
public Task WriteTextAsync(string text, CancellationToken cancellationToken = default)
|
||||
=> WriteTextAsync(text, TextEncoding, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Writes text asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <param name="encoding">The encoding.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous write operation.</returns>
|
||||
public Task WriteTextAsync(string text, Encoding encoding, CancellationToken cancellationToken = default)
|
||||
=> WriteDataAsync(encoding.GetBytes(text), true, cancellationToken);
|
||||
|
||||
/// <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="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous write operation.</returns>
|
||||
public Task WriteLineAsync(string line, Encoding encoding, CancellationToken cancellationToken = default)
|
||||
=> WriteDataAsync(encoding.GetBytes($"{line}{_newLineSequence}"), true, cancellationToken);
|
||||
|
||||
/// <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="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous write operation.</returns>
|
||||
public Task WriteLineAsync(string line, CancellationToken cancellationToken = default)
|
||||
=> WriteLineAsync(line, TextEncoding, cancellationToken);
|
||||
|
||||
#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 !NET461
|
||||
RemoteClient.Dispose();
|
||||
SecureStream?.Dispose();
|
||||
NetworkStream?.Dispose();
|
||||
#else
|
||||
RemoteClient.Close();
|
||||
SecureStream?.Close();
|
||||
NetworkStream?.Close();
|
||||
#endif
|
||||
}
|
||||
finally
|
||||
{
|
||||
NetworkStream = null;
|
||||
SecureStream = null;
|
||||
RemoteClient = null;
|
||||
_continuousReadingThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Dispose
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (_hasDisposed)
|
||||
return;
|
||||
|
||||
// Release managed resources
|
||||
Disconnect();
|
||||
_continuousReadingThread = null;
|
||||
_writeDone.Dispose();
|
||||
|
||||
_hasDisposed = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Continuous Read Methods
|
||||
|
||||
private void RaiseReceiveBufferEvents(IEnumerable<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(PerformContinuousReading), "Continuous Read operation errored");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (doThreadSleep)
|
||||
Thread.Sleep(_continuousReadingInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace Swan
|
||||
{
|
||||
/// <summary>
|
||||
/// Enumerates the possible causes of the DataReceived event occurring.
|
||||
/// </summary>
|
||||
public enum ConnectionDataReceivedTrigger
|
||||
{
|
||||
/// <summary>
|
||||
/// The trigger was a forceful flush of the buffer
|
||||
/// </summary>
|
||||
Flush,
|
||||
|
||||
/// <summary>
|
||||
/// The new line sequence bytes were received
|
||||
/// </summary>
|
||||
NewLineSequenceEncountered,
|
||||
|
||||
/// <summary>
|
||||
/// The buffer was full
|
||||
/// </summary>
|
||||
BufferFull,
|
||||
|
||||
/// <summary>
|
||||
/// The block size reached
|
||||
/// </summary>
|
||||
BlockSizeReached,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
namespace Swan.Net
|
||||
{
|
||||
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
|
||||
{
|
||||
private readonly object _stateLock = new object();
|
||||
private TcpListener _listenerSocket;
|
||||
private bool _cancellationPending;
|
||||
private CancellationTokenSource _cancelListening;
|
||||
private Task? _backgroundWorkerTask;
|
||||
private bool _hasDisposed;
|
||||
|
||||
#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 !NET461
|
||||
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,62 @@
|
||||
namespace Swan.Net.Dns
|
||||
{
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
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
|
||||
{
|
||||
Task<DnsClientResponse> Request(DnsClientRequest request);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,681 @@
|
||||
namespace Swan.Net.Dns
|
||||
{
|
||||
using Formatters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
/// <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 async Task<DnsClientResponse> Resolve()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _resolver.Request(this).ConfigureAwait(false);
|
||||
|
||||
if (response.Id != Id)
|
||||
{
|
||||
throw new DnsQueryException(response, "Mismatching request/response IDs");
|
||||
}
|
||||
|
||||
if (response.ResponseCode != DnsResponseCode.NoError)
|
||||
{
|
||||
throw new DnsQueryException(response);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (e is ArgumentException || e is SocketException)
|
||||
throw new DnsQueryException("Invalid response", e);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class DnsRequest : IDnsRequest
|
||||
{
|
||||
private static readonly Random Random = new Random();
|
||||
|
||||
private DnsHeader header;
|
||||
|
||||
public DnsRequest()
|
||||
{
|
||||
Questions = new List<DnsQuestion>();
|
||||
header = new DnsHeader
|
||||
{
|
||||
OperationCode = DnsOperationCode.Query,
|
||||
Response = false,
|
||||
Id = Random.Next(ushort.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 { get; }
|
||||
|
||||
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();
|
||||
using var result = new MemoryStream(Size);
|
||||
|
||||
return result
|
||||
.Append(header.ToArray())
|
||||
.Append(Questions.Select(q => q.ToArray()))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
UpdateHeader();
|
||||
|
||||
return Json.Serialize(this, true);
|
||||
}
|
||||
|
||||
private void UpdateHeader()
|
||||
{
|
||||
header.QuestionCount = Questions.Count;
|
||||
}
|
||||
}
|
||||
|
||||
public class DnsTcpRequestResolver : IDnsRequestResolver
|
||||
{
|
||||
public async Task<DnsClientResponse> Request(DnsClientRequest request)
|
||||
{
|
||||
var tcp = new TcpClient();
|
||||
|
||||
try
|
||||
{
|
||||
#if !NET461
|
||||
await tcp.Client.ConnectAsync(request.Dns).ConfigureAwait(false);
|
||||
#else
|
||||
tcp.Client.Connect(request.Dns);
|
||||
#endif
|
||||
var stream = tcp.GetStream();
|
||||
var buffer = request.ToArray();
|
||||
var length = BitConverter.GetBytes((ushort)buffer.Length);
|
||||
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(length);
|
||||
|
||||
await stream.WriteAsync(length, 0, length.Length).ConfigureAwait(false);
|
||||
await stream.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
||||
|
||||
buffer = new byte[2];
|
||||
await Read(stream, buffer).ConfigureAwait(false);
|
||||
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(buffer);
|
||||
|
||||
buffer = new byte[BitConverter.ToUInt16(buffer, 0)];
|
||||
await Read(stream, buffer).ConfigureAwait(false);
|
||||
|
||||
var response = DnsResponse.FromArray(buffer);
|
||||
|
||||
return new DnsClientResponse(request, response, buffer);
|
||||
}
|
||||
finally
|
||||
{
|
||||
#if NET461
|
||||
tcp.Close();
|
||||
#else
|
||||
tcp.Dispose();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task Read(Stream stream, byte[] buffer)
|
||||
{
|
||||
var length = buffer.Length;
|
||||
var offset = 0;
|
||||
int size;
|
||||
|
||||
while (length > 0 && (size = await stream.ReadAsync(buffer, offset, length).ConfigureAwait(false)) > 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 async Task<DnsClientResponse> Request(DnsClientRequest request)
|
||||
{
|
||||
var udp = new UdpClient();
|
||||
var dns = request.Dns;
|
||||
|
||||
try
|
||||
{
|
||||
udp.Client.SendTimeout = 7000;
|
||||
udp.Client.ReceiveTimeout = 7000;
|
||||
#if !NET461
|
||||
await udp.Client.ConnectAsync(dns).ConfigureAwait(false);
|
||||
#else
|
||||
udp.Client.Connect(dns);
|
||||
#endif
|
||||
|
||||
await udp.SendAsync(request.ToArray(), request.Size).ConfigureAwait(false);
|
||||
|
||||
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
|
||||
? await _fallback.Request(request).ConfigureAwait(false)
|
||||
: new DnsClientResponse(request, response, buffer);
|
||||
}
|
||||
finally
|
||||
{
|
||||
#if NET461
|
||||
udp.Close();
|
||||
#else
|
||||
udp.Dispose();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class DnsNullRequestResolver : IDnsRequestResolver
|
||||
{
|
||||
public Task<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;
|
||||
|
||||
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 static DnsHeader FromArray(byte[] header) =>
|
||||
header.Length < SIZE
|
||||
? throw new ArgumentException("Header length too small")
|
||||
: header.ToStruct<DnsHeader>(0, SIZE);
|
||||
|
||||
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 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)
|
||||
{
|
||||
Name = domain;
|
||||
_type = type;
|
||||
_klass = klass;
|
||||
}
|
||||
|
||||
public DnsDomain Name { get; }
|
||||
|
||||
public DnsRecordType Type => _type;
|
||||
|
||||
public DnsRecordClass Class => _klass;
|
||||
|
||||
public int Size => Name.Size + Tail.SIZE;
|
||||
|
||||
public byte[] ToArray() =>
|
||||
new MemoryStream(Size)
|
||||
.Append(Name.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,419 @@
|
||||
namespace Swan.Net.Dns
|
||||
{
|
||||
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 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() =>
|
||||
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
|
||||
=> new List<string>(base.IncludedProperties) {nameof(IPAddress)}.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
|
||||
=> new List<string>(base.IncludedProperties) {nameof(NSDomainName)}.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;
|
||||
|
||||
return record.Type switch
|
||||
{
|
||||
DnsRecordType.A => (IDnsResourceRecord) new DnsIPAddressResourceRecord(record),
|
||||
DnsRecordType.AAAA => new DnsIPAddressResourceRecord(record),
|
||||
DnsRecordType.NS => new DnsNameServerResourceRecord(record, message, dataOffset),
|
||||
DnsRecordType.CNAME => new DnsCanonicalNameResourceRecord(record, message, dataOffset),
|
||||
DnsRecordType.SOA => new DnsStartOfAuthorityResourceRecord(record, message, dataOffset),
|
||||
DnsRecordType.PTR => new DnsPointerResourceRecord(record, message, dataOffset),
|
||||
DnsRecordType.MX => new DnsMailExchangeResourceRecord(record, message, dataOffset),
|
||||
_ => record
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
namespace Swan.Net.Dns
|
||||
{
|
||||
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,79 @@
|
||||
namespace Swan.Net.Dns
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
/// <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)
|
||||
=> new DnsClientRequest(_dns, request, _resolver);
|
||||
|
||||
public async Task<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 = await Resolve(domain, type).ConfigureAwait(false);
|
||||
var ips = response.AnswerRecords
|
||||
.Where(r => r.Type == type)
|
||||
.Cast<DnsIPAddressResourceRecord>()
|
||||
.Select(r => r.IPAddress)
|
||||
.ToList();
|
||||
|
||||
return ips.Count == 0 ? throw new DnsQueryException(response, "No matching records") : ips;
|
||||
}
|
||||
|
||||
public async Task<string> Reverse(IPAddress ip)
|
||||
{
|
||||
if (ip == null)
|
||||
throw new ArgumentNullException(nameof(ip));
|
||||
|
||||
var response = await Resolve(DnsDomain.PointerName(ip), DnsRecordType.PTR);
|
||||
var ptr = response.AnswerRecords.FirstOrDefault(r => r.Type == DnsRecordType.PTR);
|
||||
|
||||
return ptr == null
|
||||
? throw new DnsQueryException(response, "No matching records")
|
||||
: ((DnsPointerResourceRecord) ptr).PointerDomainName.ToString();
|
||||
}
|
||||
|
||||
public Task<DnsClientResponse> Resolve(string domain, DnsRecordType type) =>
|
||||
Resolve(new DnsDomain(domain), type);
|
||||
|
||||
public Task<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,37 @@
|
||||
namespace Swan.Net.Dns
|
||||
{
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// An exception thrown when the DNS query fails.
|
||||
/// </summary>
|
||||
/// <seealso cref="Exception" />
|
||||
[Serializable]
|
||||
public class DnsQueryException : Exception
|
||||
{
|
||||
internal DnsQueryException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
internal DnsQueryException(string message, Exception e)
|
||||
: base(message, e)
|
||||
{
|
||||
}
|
||||
|
||||
internal DnsQueryException(DnsClient.IDnsResponse response)
|
||||
: this(response, Format(response))
|
||||
{
|
||||
}
|
||||
|
||||
internal DnsQueryException(DnsClient.IDnsResponse response, string message)
|
||||
: base(message)
|
||||
{
|
||||
Response = response;
|
||||
}
|
||||
|
||||
internal DnsClient.IDnsResponse? Response { get; }
|
||||
|
||||
private static string Format(DnsClient.IDnsResponse response) => $"Invalid response received with code {response.ResponseCode}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
namespace Swan.Net.Dns
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a response from a DNS server.
|
||||
/// </summary>
|
||||
public class DnsQueryResult
|
||||
{
|
||||
private readonly List<DnsRecord> _mAnswerRecords = new List<DnsRecord>();
|
||||
private readonly List<DnsRecord> _mAdditionalRecords = new List<DnsRecord>();
|
||||
private readonly List<DnsRecord> _mAuthorityRecords = new List<DnsRecord>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DnsQueryResult"/> class.
|
||||
/// </summary>
|
||||
/// <param name="response">The response.</param>
|
||||
internal DnsQueryResult(DnsClient.IDnsResponse 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 => _mAnswerRecords;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the additional records.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The additional records.
|
||||
/// </value>
|
||||
public IList<DnsRecord> AdditionalRecords => _mAdditionalRecords;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authority records.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The authority records.
|
||||
/// </value>
|
||||
public IList<DnsRecord> AuthorityRecords => _mAuthorityRecords;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
namespace Swan.Net.Dns
|
||||
{
|
||||
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,172 @@
|
||||
// ReSharper disable InconsistentNaming
|
||||
namespace Swan.Net.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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
namespace Swan.Net
|
||||
{
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
/// <summary>
|
||||
/// The event arguments for when connections are accepted.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.EventArgs" />
|
||||
public class ConnectionAcceptedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConnectionAcceptedEventArgs" /> class.
|
||||
/// </summary>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <exception cref="ArgumentNullException">client.</exception>
|
||||
public ConnectionAcceptedEventArgs(TcpClient client)
|
||||
{
|
||||
Client = client ?? throw new ArgumentNullException(nameof(client));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the client.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The client.
|
||||
/// </value>
|
||||
public TcpClient Client { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs before a connection is accepted. Set the Cancel property to true to prevent the connection from being accepted.
|
||||
/// </summary>
|
||||
/// <seealso cref="ConnectionAcceptedEventArgs" />
|
||||
public class ConnectionAcceptingEventArgs : ConnectionAcceptedEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConnectionAcceptingEventArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="client">The client.</param>
|
||||
public ConnectionAcceptingEventArgs(TcpClient client)
|
||||
: base(client)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setting Cancel to true rejects the new TcpClient.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if cancel; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool Cancel { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event arguments for when a server listener is started.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.EventArgs" />
|
||||
public class ConnectionListenerStartedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConnectionListenerStartedEventArgs" /> class.
|
||||
/// </summary>
|
||||
/// <param name="listenerEndPoint">The listener end point.</param>
|
||||
/// <exception cref="ArgumentNullException">listenerEndPoint.</exception>
|
||||
public ConnectionListenerStartedEventArgs(IPEndPoint listenerEndPoint)
|
||||
{
|
||||
EndPoint = listenerEndPoint ?? throw new ArgumentNullException(nameof(listenerEndPoint));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the end point.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The end point.
|
||||
/// </value>
|
||||
public IPEndPoint EndPoint { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event arguments for when a server listener fails to start.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.EventArgs" />
|
||||
public class ConnectionListenerFailedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConnectionListenerFailedEventArgs" /> class.
|
||||
/// </summary>
|
||||
/// <param name="listenerEndPoint">The listener end point.</param>
|
||||
/// <param name="ex">The ex.</param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// listenerEndPoint
|
||||
/// or
|
||||
/// ex.
|
||||
/// </exception>
|
||||
public ConnectionListenerFailedEventArgs(IPEndPoint listenerEndPoint, Exception ex)
|
||||
{
|
||||
EndPoint = listenerEndPoint ?? throw new ArgumentNullException(nameof(listenerEndPoint));
|
||||
Error = ex ?? throw new ArgumentNullException(nameof(ex));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the end point.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The end point.
|
||||
/// </value>
|
||||
public IPEndPoint EndPoint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the error.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The error.
|
||||
/// </value>
|
||||
public Exception Error { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event arguments for when a server listener stopped.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.EventArgs" />
|
||||
public class ConnectionListenerStoppedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConnectionListenerStoppedEventArgs" /> class.
|
||||
/// </summary>
|
||||
/// <param name="listenerEndPoint">The listener end point.</param>
|
||||
/// <param name="ex">The ex.</param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// listenerEndPoint
|
||||
/// or
|
||||
/// ex.
|
||||
/// </exception>
|
||||
public ConnectionListenerStoppedEventArgs(IPEndPoint listenerEndPoint, Exception? ex = null)
|
||||
{
|
||||
EndPoint = listenerEndPoint ?? throw new ArgumentNullException(nameof(listenerEndPoint));
|
||||
Error = ex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the end point.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The end point.
|
||||
/// </value>
|
||||
public IPEndPoint EndPoint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the error.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The error.
|
||||
/// </value>
|
||||
public Exception? Error { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
namespace Swan.Net
|
||||
{
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
/// <summary>
|
||||
/// The event arguments for connection failure events.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.EventArgs" />
|
||||
public class ConnectionFailureEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConnectionFailureEventArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="ex">The ex.</param>
|
||||
public ConnectionFailureEventArgs(Exception ex)
|
||||
{
|
||||
Error = ex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the error.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The error.
|
||||
/// </value>
|
||||
public Exception Error { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event arguments for when data is received.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.EventArgs" />
|
||||
public class ConnectionDataReceivedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConnectionDataReceivedEventArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer.</param>
|
||||
/// <param name="trigger">The trigger.</param>
|
||||
/// <param name="moreAvailable">if set to <c>true</c> [more available].</param>
|
||||
public ConnectionDataReceivedEventArgs(byte[] buffer, ConnectionDataReceivedTrigger trigger, bool moreAvailable)
|
||||
{
|
||||
Buffer = buffer ?? throw new ArgumentNullException(nameof(buffer));
|
||||
Trigger = trigger;
|
||||
HasMoreAvailable = moreAvailable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the buffer.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The buffer.
|
||||
/// </value>
|
||||
public byte[] Buffer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cause as to why this event was thrown.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The trigger.
|
||||
/// </value>
|
||||
public ConnectionDataReceivedTrigger Trigger { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the receive buffer has more bytes available.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance has more available; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool HasMoreAvailable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the string from buffer.
|
||||
/// </summary>
|
||||
/// <param name="encoding">The encoding.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="System.String" /> that contains the results of decoding the specified sequence of bytes.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">encoding</exception>
|
||||
public string GetStringFromBuffer(Encoding encoding)
|
||||
=> encoding?.GetString(Buffer).TrimEnd('\r', '\n') ?? throw new ArgumentNullException(nameof(encoding));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
namespace Swan.Net
|
||||
{
|
||||
using Formatters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
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";
|
||||
private const string FormType = "application/x-www-form-urlencoded";
|
||||
|
||||
private static readonly HttpClient HttpClient = new HttpClient();
|
||||
|
||||
/// <summary>
|
||||
/// Post a object as JSON with optional authorization token.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of response object.</typeparam>
|
||||
/// <param name="requestUri">The request URI.</param>
|
||||
/// <param name="payload">The payload.</param>
|
||||
/// <param name="authorization">The authorization.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// A task with a result of the requested type.
|
||||
/// </returns>
|
||||
public static async Task<T> Post<T>(
|
||||
Uri requestUri,
|
||||
object payload,
|
||||
string? authorization = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var jsonString = await PostString(requestUri, payload, authorization, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return !string.IsNullOrEmpty(jsonString) ? Json.Deserialize<T>(jsonString) : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts the specified URL.
|
||||
/// </summary>
|
||||
/// <param name="requestUri">The request URI.</param>
|
||||
/// <param name="payload">The payload.</param>
|
||||
/// <param name="authorization">The authorization.</param>
|
||||
/// <param name="cancellationToken">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(
|
||||
Uri requestUri,
|
||||
object payload,
|
||||
string? authorization = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var jsonString = await PostString(requestUri, payload, authorization, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return string.IsNullOrWhiteSpace(jsonString)
|
||||
? default
|
||||
: Json.Deserialize(jsonString) as IDictionary<string, object>;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts the specified URL.
|
||||
/// </summary>
|
||||
/// <param name="requestUri">The request URI.</param>
|
||||
/// <param name="payload">The payload.</param>
|
||||
/// <param name="authorization">The authorization.</param>
|
||||
/// <param name="cancellationToken">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(
|
||||
Uri requestUri,
|
||||
object payload,
|
||||
string? authorization = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> SendAsync(HttpMethod.Post, requestUri, payload, authorization, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Puts the specified URL.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of response object.</typeparam>
|
||||
/// <param name="requestUri">The request URI.</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>(
|
||||
Uri requestUri,
|
||||
object payload,
|
||||
string? authorization = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var jsonString = await PutString(requestUri, payload, authorization, ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return !string.IsNullOrEmpty(jsonString) ? Json.Deserialize<T>(jsonString) : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Puts the specified URL.
|
||||
/// </summary>
|
||||
/// <param name="requestUri">The request URI.</param>
|
||||
/// <param name="payload">The payload.</param>
|
||||
/// <param name="authorization">The authorization.</param>
|
||||
/// <param name="cancellationToken">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(
|
||||
Uri requestUri,
|
||||
object payload,
|
||||
string? authorization = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await Put<object>(requestUri, payload, authorization, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return response as IDictionary<string, object>;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Puts as string.
|
||||
/// </summary>
|
||||
/// <param name="requestUri">The request URI.</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(
|
||||
Uri requestUri,
|
||||
object payload,
|
||||
string? authorization = null,
|
||||
CancellationToken ct = default) => SendAsync(HttpMethod.Put, requestUri, payload, authorization, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Gets as string.
|
||||
/// </summary>
|
||||
/// <param name="requestUri">The request URI.</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 Task<string> GetString(
|
||||
Uri requestUri,
|
||||
string? authorization = null,
|
||||
CancellationToken ct = default)
|
||||
=> GetString(requestUri, null, authorization, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the string.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI.</param>
|
||||
/// <param name="headers">The headers.</param>
|
||||
/// <param name="authorization">The authorization.</param>
|
||||
/// <param name="ct">The ct.</param>
|
||||
/// <returns>
|
||||
/// A task with a result of the requested string.
|
||||
/// </returns>
|
||||
public static async Task<string> GetString(
|
||||
Uri uri,
|
||||
IDictionary<string, IEnumerable<string>>? headers,
|
||||
string? authorization = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var response = await GetHttpContent(uri, ct, authorization, headers)
|
||||
.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="requestUri">The request URI.</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>(
|
||||
Uri requestUri,
|
||||
string? authorization = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var jsonString = await GetString(requestUri, authorization, ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return !string.IsNullOrEmpty(jsonString) ? Json.Deserialize<T>(jsonString) : default;
|
||||
}
|
||||
|
||||
/// <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="requestUri">The request URI.</param>
|
||||
/// <param name="headers">The headers.</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>(
|
||||
Uri requestUri,
|
||||
IDictionary<string, IEnumerable<string>>? headers,
|
||||
string? authorization = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var jsonString = await GetString(requestUri, headers, authorization, ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return !string.IsNullOrEmpty(jsonString) ? Json.Deserialize<T>(jsonString) : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the binary.
|
||||
/// </summary>
|
||||
/// <param name="requestUri">The request URI.</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(
|
||||
Uri requestUri,
|
||||
string? authorization = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var response = await GetHttpContent(requestUri, ct, authorization)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await response.ReadAsByteArrayAsync()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authenticate against a web server using Bearer Token.
|
||||
/// </summary>
|
||||
/// <param name="requestUri">The request URI.</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(
|
||||
Uri requestUri,
|
||||
string username,
|
||||
string password,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
throw new ArgumentNullException(nameof(username));
|
||||
|
||||
// ignore empty password for now
|
||||
var content = $"grant_type=password&username={username}&password={password}";
|
||||
using var requestContent = new StringContent(content, Encoding.UTF8, FormType);
|
||||
var response = await HttpClient.PostAsync(requestUri, requestContent, ct).ConfigureAwait(false);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
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="requestUri">The request URI.</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(
|
||||
Uri requestUri,
|
||||
byte[] buffer,
|
||||
string fileName,
|
||||
string? authorization = null,
|
||||
CancellationToken ct = default) =>
|
||||
PostString(requestUri, new { Filename = fileName, Data = buffer }, authorization, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Posts the file.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The response type.</typeparam>
|
||||
/// <param name="requestUri">The request URI.</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>(
|
||||
Uri requestUri,
|
||||
byte[] buffer,
|
||||
string fileName,
|
||||
string? authorization = null,
|
||||
CancellationToken ct = default) =>
|
||||
Post<T>(requestUri, new { Filename = fileName, Data = buffer }, authorization, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Sends the asynchronous request.
|
||||
/// </summary>
|
||||
/// <param name="method">The method.</param>
|
||||
/// <param name="requestUri">The request URI.</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">requestUri.</exception>
|
||||
/// <exception cref="JsonRequestException">Error {method} JSON.</exception>
|
||||
public static async Task<string> SendAsync(
|
||||
HttpMethod method,
|
||||
Uri requestUri,
|
||||
object payload,
|
||||
string? authorization = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
using var response = await GetResponse(requestUri, authorization, null, payload, method, ct).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new JsonRequestException(
|
||||
$"Error {method} JSON",
|
||||
(int)response.StatusCode,
|
||||
await response.Content.ReadAsStringAsync().ConfigureAwait(false));
|
||||
}
|
||||
|
||||
return await response.Content.ReadAsStringAsync()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task<HttpContent> GetHttpContent(
|
||||
Uri uri,
|
||||
CancellationToken ct,
|
||||
string? authorization = null,
|
||||
IDictionary<string, IEnumerable<string>>? headers = null)
|
||||
{
|
||||
var response = await GetResponse(uri, authorization, headers, ct: ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return response.IsSuccessStatusCode
|
||||
? response.Content
|
||||
: throw new JsonRequestException("Error GET", (int)response.StatusCode);
|
||||
}
|
||||
|
||||
private static async Task<HttpResponseMessage> GetResponse(
|
||||
Uri uri,
|
||||
string? authorization,
|
||||
IDictionary<string, IEnumerable<string>>? headers,
|
||||
object? payload = null,
|
||||
HttpMethod? method = default,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (uri == null)
|
||||
throw new ArgumentNullException(nameof(uri));
|
||||
|
||||
using var requestMessage = new HttpRequestMessage(method ?? HttpMethod.Get, uri);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(authorization))
|
||||
{
|
||||
requestMessage.Headers.Authorization
|
||||
= new AuthenticationHeaderValue("Bearer", authorization);
|
||||
}
|
||||
|
||||
if (headers != null)
|
||||
{
|
||||
foreach (var header in headers)
|
||||
requestMessage.Headers.Add(header.Key, header.Value);
|
||||
}
|
||||
|
||||
if (payload != null && requestMessage.Method != HttpMethod.Get)
|
||||
{
|
||||
requestMessage.Content = new StringContent(Json.Serialize(payload), Encoding.UTF8, JsonMimeType);
|
||||
}
|
||||
|
||||
return await HttpClient.SendAsync(requestMessage, ct)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
namespace Swan.Net
|
||||
{
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Represents errors that occurs requesting a JSON file through HTTP.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.Exception" />
|
||||
[Serializable]
|
||||
public class JsonRequestException
|
||||
: Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JsonRequestException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="httpErrorCode">The HTTP error code.</param>
|
||||
/// <param name="errorContent">Content of the error.</param>
|
||||
public JsonRequestException(string message, int httpErrorCode = 500, string errorContent = null)
|
||||
: base(message)
|
||||
{
|
||||
HttpErrorCode = httpErrorCode;
|
||||
HttpErrorContent = errorContent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the HTTP error code.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The HTTP error code.
|
||||
/// </value>
|
||||
public int HttpErrorCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the content of the HTTP error.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The content of the HTTP error.
|
||||
/// </value>
|
||||
public string HttpErrorContent { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => string.IsNullOrEmpty(HttpErrorContent)
|
||||
? $"HTTP Response Status Code {HttpErrorCode} Error Message: {HttpErrorContent}"
|
||||
: base.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
namespace Swan.Net
|
||||
{
|
||||
using Net.Dns;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Provides miscellaneous network utilities such as a Public IP finder,
|
||||
/// a DNS client to query DNS records of any kind, and an NTP client.
|
||||
/// </summary>
|
||||
public static class Network
|
||||
{
|
||||
/// <summary>
|
||||
/// The DNS default port.
|
||||
/// </summary>
|
||||
public const int DnsDefaultPort = 53;
|
||||
|
||||
/// <summary>
|
||||
/// The NTP default port.
|
||||
/// </summary>
|
||||
public const int NtpDefaultPort = 123;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the host.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name of the host.
|
||||
/// </value>
|
||||
public static string HostName => IPGlobalProperties.GetIPGlobalProperties().HostName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the network domain.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The name of the network domain.
|
||||
/// </value>
|
||||
public static string DomainName => IPGlobalProperties.GetIPGlobalProperties().DomainName;
|
||||
|
||||
#region IP Addresses and Adapters Information Methods
|
||||
|
||||
/// <summary>
|
||||
/// Gets the active IPv4 interfaces.
|
||||
/// Only those interfaces with a valid unicast address and a valid gateway will be returned in the collection.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A collection of NetworkInterface/IPInterfaceProperties pairs
|
||||
/// that represents the active IPv4 interfaces.
|
||||
/// </returns>
|
||||
public static Dictionary<NetworkInterface, IPInterfaceProperties> GetIPv4Interfaces()
|
||||
{
|
||||
// zero conf ip address
|
||||
var zeroConf = new IPAddress(0);
|
||||
|
||||
var adapters = NetworkInterface.GetAllNetworkInterfaces()
|
||||
.Where(network =>
|
||||
network.OperationalStatus == OperationalStatus.Up
|
||||
&& network.NetworkInterfaceType != NetworkInterfaceType.Unknown
|
||||
&& network.NetworkInterfaceType != NetworkInterfaceType.Loopback)
|
||||
.ToArray();
|
||||
|
||||
var result = new Dictionary<NetworkInterface, IPInterfaceProperties>();
|
||||
|
||||
foreach (var adapter in adapters)
|
||||
{
|
||||
var properties = adapter.GetIPProperties();
|
||||
if (properties == null
|
||||
|| properties.GatewayAddresses.Count == 0
|
||||
|| properties.GatewayAddresses.All(gateway => Equals(gateway.Address, zeroConf))
|
||||
|| properties.UnicastAddresses.Count == 0
|
||||
|| properties.GatewayAddresses.All(address => Equals(address.Address, zeroConf))
|
||||
|| properties.UnicastAddresses.Any(a => a.Address.AddressFamily == AddressFamily.InterNetwork) ==
|
||||
false)
|
||||
continue;
|
||||
|
||||
result[adapter] = properties;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the local ip addresses.
|
||||
/// </summary>
|
||||
/// <param name="includeLoopback">if set to <c>true</c> [include loopback].</param>
|
||||
/// <returns>An array of local ip addresses.</returns>
|
||||
public static IPAddress[] GetIPv4Addresses(bool includeLoopback = true) =>
|
||||
GetIPv4Addresses(NetworkInterfaceType.Unknown, true, includeLoopback);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the local ip addresses.
|
||||
/// </summary>
|
||||
/// <param name="interfaceType">Type of the interface.</param>
|
||||
/// <param name="skipTypeFilter">if set to <c>true</c> [skip type filter].</param>
|
||||
/// <param name="includeLoopback">if set to <c>true</c> [include loopback].</param>
|
||||
/// <returns>An array of local ip addresses.</returns>
|
||||
public static IPAddress[] GetIPv4Addresses(
|
||||
NetworkInterfaceType interfaceType,
|
||||
bool skipTypeFilter = false,
|
||||
bool includeLoopback = false)
|
||||
{
|
||||
var addressList = new List<IPAddress>();
|
||||
var interfaces = NetworkInterface.GetAllNetworkInterfaces()
|
||||
.Where(ni =>
|
||||
#if NET461
|
||||
ni.IsReceiveOnly == false &&
|
||||
#endif
|
||||
(skipTypeFilter || ni.NetworkInterfaceType == interfaceType) &&
|
||||
ni.OperationalStatus == OperationalStatus.Up)
|
||||
.ToArray();
|
||||
|
||||
foreach (var networkInterface in interfaces)
|
||||
{
|
||||
var properties = networkInterface.GetIPProperties();
|
||||
|
||||
if (properties.GatewayAddresses.All(g => g.Address.AddressFamily != AddressFamily.InterNetwork))
|
||||
continue;
|
||||
|
||||
addressList.AddRange(properties.UnicastAddresses
|
||||
.Where(i => i.Address.AddressFamily == AddressFamily.InterNetwork)
|
||||
.Select(i => i.Address));
|
||||
}
|
||||
|
||||
if (includeLoopback || interfaceType == NetworkInterfaceType.Loopback)
|
||||
addressList.Add(IPAddress.Loopback);
|
||||
|
||||
return addressList.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the public IP address using ipify.org.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A public IP address of the result produced by this Task.</returns>
|
||||
public static async Task<IPAddress> GetPublicIPAddressAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
var response = await client.GetAsync("https://api.ipify.org", cancellationToken).ConfigureAwait(false);
|
||||
return IPAddress.Parse(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configured IPv4 DNS servers for the active network interfaces.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A collection of NetworkInterface/IPInterfaceProperties pairs
|
||||
/// that represents the active IPv4 interfaces.
|
||||
/// </returns>
|
||||
public static IPAddress[] GetIPv4DnsServers()
|
||||
=> GetIPv4Interfaces()
|
||||
.Select(a => a.Value.DnsAddresses.Where(d => d.AddressFamily == AddressFamily.InterNetwork))
|
||||
.SelectMany(d => d)
|
||||
.ToArray();
|
||||
|
||||
#endregion
|
||||
|
||||
#region DNS and NTP Clients
|
||||
|
||||
/// <summary>
|
||||
/// Gets the DNS host entry (a list of IP addresses) for the domain name.
|
||||
/// </summary>
|
||||
/// <param name="fqdn">The FQDN.</param>
|
||||
/// <returns>An array of local ip addresses of the result produced by this task.</returns>
|
||||
public static Task<IPAddress[]> GetDnsHostEntryAsync(string fqdn)
|
||||
{
|
||||
var dnsServer = GetIPv4DnsServers().FirstOrDefault() ?? IPAddress.Parse("8.8.8.8");
|
||||
return GetDnsHostEntryAsync(fqdn, dnsServer, DnsDefaultPort);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the DNS host entry (a list of IP addresses) for the domain name.
|
||||
/// </summary>
|
||||
/// <param name="fqdn">The FQDN.</param>
|
||||
/// <param name="dnsServer">The DNS server.</param>
|
||||
/// <param name="port">The port.</param>
|
||||
/// <returns>
|
||||
/// An array of local ip addresses of the result produced by this task.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">fqdn.</exception>
|
||||
public static async Task<IPAddress[]> GetDnsHostEntryAsync(string fqdn, IPAddress dnsServer, int port)
|
||||
{
|
||||
if (fqdn == null)
|
||||
throw new ArgumentNullException(nameof(fqdn));
|
||||
|
||||
if (fqdn.IndexOf(".", StringComparison.Ordinal) == -1)
|
||||
{
|
||||
fqdn += "." + IPGlobalProperties.GetIPGlobalProperties().DomainName;
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (!fqdn.EndsWith(".", StringComparison.OrdinalIgnoreCase)) break;
|
||||
|
||||
fqdn = fqdn.Substring(0, fqdn.Length - 1);
|
||||
}
|
||||
|
||||
var client = new DnsClient(dnsServer, port);
|
||||
var result = await client.Lookup(fqdn).ConfigureAwait(false);
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reverse lookup FQDN of the given IP Address.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <param name="dnsServer">The DNS server.</param>
|
||||
/// <param name="port">The port.</param>
|
||||
/// <returns>A <see cref="System.String" /> that represents the current object.</returns>
|
||||
public static Task<string> GetDnsPointerEntryAsync(IPAddress query, IPAddress dnsServer, int port)
|
||||
{
|
||||
var client = new DnsClient(dnsServer, port);
|
||||
return client.Reverse(query);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reverse lookup FQDN of the given IP Address.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <returns>A <see cref="System.String" /> that represents the current object.</returns>
|
||||
public static Task<string> GetDnsPointerEntryAsync(IPAddress query)
|
||||
{
|
||||
var client = new DnsClient(GetIPv4DnsServers().FirstOrDefault());
|
||||
return client.Reverse(query);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queries the DNS server for the specified record type.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <param name="recordType">Type of the record.</param>
|
||||
/// <param name="dnsServer">The DNS server.</param>
|
||||
/// <param name="port">The port.</param>
|
||||
/// <returns>Queries the DNS server for the specified record type of the result produced by this Task.</returns>
|
||||
public static async Task<DnsQueryResult> QueryDnsAsync(string query, DnsRecordType recordType, IPAddress dnsServer, int port)
|
||||
{
|
||||
if (query == null)
|
||||
throw new ArgumentNullException(nameof(query));
|
||||
|
||||
var client = new DnsClient(dnsServer, port);
|
||||
var response = await client.Resolve(query, recordType).ConfigureAwait(false);
|
||||
return new DnsQueryResult(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queries the DNS server for the specified record type.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <param name="recordType">Type of the record.</param>
|
||||
/// <returns>Queries the DNS server for the specified record type of the result produced by this Task.</returns>
|
||||
public static Task<DnsQueryResult> QueryDnsAsync(string query, DnsRecordType recordType) => QueryDnsAsync(query, recordType, GetIPv4DnsServers().FirstOrDefault(), DnsDefaultPort);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the UTC time by querying from an NTP server.
|
||||
/// </summary>
|
||||
/// <param name="ntpServerAddress">The NTP server address.</param>
|
||||
/// <param name="port">The port.</param>
|
||||
/// <returns>The UTC time by querying from an NTP server of the result produced by this Task.</returns>
|
||||
public static async Task<DateTime> GetNetworkTimeUtcAsync(IPAddress ntpServerAddress, int port = NtpDefaultPort)
|
||||
{
|
||||
if (ntpServerAddress == null)
|
||||
throw new ArgumentNullException(nameof(ntpServerAddress));
|
||||
|
||||
// NTP message size - 16 bytes of the digest (RFC 2030)
|
||||
var ntpData = new byte[48];
|
||||
|
||||
// Setting the Leap Indicator, Version Number and Mode values
|
||||
ntpData[0] = 0x1B; // LI = 0 (no warning), VN = 3 (IPv4 only), Mode = 3 (Client Mode)
|
||||
|
||||
// The UDP port number assigned to NTP is 123
|
||||
var endPoint = new IPEndPoint(ntpServerAddress, port);
|
||||
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
||||
|
||||
#if !NET461
|
||||
await socket.ConnectAsync(endPoint).ConfigureAwait(false);
|
||||
#else
|
||||
socket.Connect(endPoint);
|
||||
#endif
|
||||
|
||||
socket.ReceiveTimeout = 3000; // Stops code hang if NTP is blocked
|
||||
socket.Send(ntpData);
|
||||
socket.Receive(ntpData);
|
||||
socket.Dispose();
|
||||
|
||||
// Offset to get to the "Transmit Timestamp" field (time at which the reply
|
||||
// departed the server for the client, in 64-bit timestamp format."
|
||||
const byte serverReplyTime = 40;
|
||||
|
||||
// Get the seconds part
|
||||
ulong intPart = BitConverter.ToUInt32(ntpData, serverReplyTime);
|
||||
|
||||
// Get the seconds fraction
|
||||
ulong fractPart = BitConverter.ToUInt32(ntpData, serverReplyTime + 4);
|
||||
|
||||
// Convert From big-endian to little-endian to match the platform
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
intPart = intPart.SwapEndianness();
|
||||
fractPart = intPart.SwapEndianness();
|
||||
}
|
||||
|
||||
var milliseconds = (intPart * 1000) + ((fractPart * 1000) / 0x100000000L);
|
||||
|
||||
// The time is given in UTC
|
||||
return new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds((long) milliseconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the UTC time by querying from an NTP server.
|
||||
/// </summary>
|
||||
/// <param name="ntpServerName">The NTP server, by default pool.ntp.org.</param>
|
||||
/// <param name="port">The port, by default NTP 123.</param>
|
||||
/// <returns>The UTC time by querying from an NTP server of the result produced by this Task.</returns>
|
||||
public static async Task<DateTime> GetNetworkTimeUtcAsync(string ntpServerName = "pool.ntp.org",
|
||||
int port = NtpDefaultPort)
|
||||
{
|
||||
var addresses = await GetDnsHostEntryAsync(ntpServerName).ConfigureAwait(false);
|
||||
return await GetNetworkTimeUtcAsync(addresses.First(), port).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
// ReSharper disable InconsistentNaming
|
||||
namespace Swan.Net.Smtp
|
||||
{
|
||||
/// <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,388 @@
|
||||
namespace Swan.Net.Smtp
|
||||
{
|
||||
using System.Threading;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using System.Net.Security;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Mail;
|
||||
|
||||
/// <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 Swan.Net.Smtp.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>
|
||||
/// using Swan.Net.Smtp;
|
||||
///
|
||||
/// 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 using MimeKit:
|
||||
/// <code>
|
||||
/// using MimeKit;
|
||||
/// using Swan.Net.Smtp;
|
||||
///
|
||||
/// 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; }
|
||||
|
||||
/// <summary>
|
||||
/// Sends an email message asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="sessionId">The session identifier.</param>
|
||||
/// <param name="callback">The callback.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</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,
|
||||
RemoteCertificateValidationCallback? callback = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
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, callback, cancellationToken);
|
||||
}
|
||||
|
||||
/// <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="callback">The callback.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</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,
|
||||
RemoteCertificateValidationCallback? callback = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (sessionState == null)
|
||||
throw new ArgumentNullException(nameof(sessionState));
|
||||
|
||||
return SendMailAsync(new[] { sessionState }, sessionId, callback, cancellationToken);
|
||||
}
|
||||
|
||||
/// <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="callback">The callback.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</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,
|
||||
RemoteCertificateValidationCallback? callback = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
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(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// EHLO 1
|
||||
await SendEhlo(sender, connection, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// STARTTLS
|
||||
if (EnableSsl)
|
||||
{
|
||||
sender.RequestText = $"{SmtpCommandNames.STARTTLS}";
|
||||
|
||||
await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
|
||||
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).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(sender, connection, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// AUTH
|
||||
if (Credentials != null)
|
||||
{
|
||||
var auth = new ConnectionAuth(connection, sender, Credentials);
|
||||
await auth.AuthenticateAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var sessionState in sessionStates)
|
||||
{
|
||||
{
|
||||
// MAIL FROM
|
||||
sender.RequestText = $"{SmtpCommandNames.MAIL} FROM:<{sessionState.SenderAddress}>";
|
||||
|
||||
await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
|
||||
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
|
||||
sender.ValidateReply();
|
||||
}
|
||||
|
||||
// RCPT TO
|
||||
foreach (var recipient in sessionState.Recipients)
|
||||
{
|
||||
sender.RequestText = $"{SmtpCommandNames.RCPT} TO:<{recipient}>";
|
||||
|
||||
await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
|
||||
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
|
||||
sender.ValidateReply();
|
||||
}
|
||||
|
||||
{
|
||||
// DATA
|
||||
sender.RequestText = $"{SmtpCommandNames.DATA}";
|
||||
|
||||
await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
|
||||
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).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, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!dataTerminator.EndsWith(SmtpDefinitions.SmtpDataCommandTerminator))
|
||||
await connection.WriteTextAsync(SmtpDefinitions.SmtpDataCommandTerminator, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
|
||||
sender.ValidateReply();
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// QUIT
|
||||
sender.RequestText = $"{SmtpCommandNames.QUIT}";
|
||||
|
||||
await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
|
||||
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
|
||||
sender.ValidateReply();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new SmtpException($"Could not send email - Session ID {sessionId}. {ex.Message}\r\n Last Request: {sender.RequestText}\r\n Last Reply: {sender.ReplyText}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendEhlo(SmtpSender sender, Connection connection, CancellationToken cancellationToken)
|
||||
{
|
||||
sender.RequestText = $"{SmtpCommandNames.EHLO} {ClientHostname}";
|
||||
|
||||
await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
do
|
||||
{
|
||||
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).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 Swan.Net.Smtp
|
||||
{
|
||||
/// <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 Swan.Net.Smtp
|
||||
{
|
||||
using Logging;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Mail;
|
||||
|
||||
/// <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}".Trace(typeof(SmtpClient), _sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
public string ReplyText { get; set; }
|
||||
|
||||
public bool IsReplyOk => ReplyText.StartsWith("250 ", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
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}".Trace(typeof(SmtpClient), _sessionId);
|
||||
|
||||
if (response.IsPositive) return;
|
||||
|
||||
var responseContent = response.Content.Any()
|
||||
? string.Join(";", response.Content.ToArray())
|
||||
: string.Empty;
|
||||
|
||||
throw new SmtpException((SmtpStatusCode)response.ReplyCode, responseContent);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!(ex is SmtpException))
|
||||
throw new SmtpException($"Could not parse server response: {ReplyText}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
namespace Swan.Net.Smtp
|
||||
{
|
||||
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,158 @@
|
||||
namespace Swan.Net.Smtp
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the state of an SMTP session associated with a client.
|
||||
/// </summary>
|
||||
public class SmtpSessionState
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SmtpSessionState"/> class.
|
||||
/// </summary>
|
||||
public SmtpSessionState()
|
||||
{
|
||||
DataBuffer = new List<byte>();
|
||||
Reset(true);
|
||||
ResetAuthentication();
|
||||
}
|
||||
|
||||
#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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user