This commit is contained in:
2019-12-04 18:57:18 +01:00
parent 4692422c9a
commit 6263791dff
225 changed files with 33065 additions and 2 deletions
+886
View File
@@ -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
}
}
+28
View File
@@ -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,
}
}
+253
View File
@@ -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
}
}
+62
View File
@@ -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);
}
}
}
+681
View File
@@ -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;
}
}
}
}
}
+419
View File
@@ -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
};
}
}
}
}
+215
View File
@@ -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;
}
}
}
}
+79
View File
@@ -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();
}
}
}
+37
View File
@@ -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}";
}
}
+123
View File
@@ -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;
}
}
+208
View File
@@ -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; }
}
}
+172
View File
@@ -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,
}
}
+158
View File
@@ -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; }
}
}
+84
View File
@@ -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));
}
}
+418
View File
@@ -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);
}
}
}
+47
View File
@@ -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();
}
}
+328
View File
@@ -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
}
}
+166
View File
@@ -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,
}
}
+388
View File
@@ -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();
}
}
}
}
+29
View File
@@ -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";
}
}
}
+60
View File
@@ -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}");
}
}
}
}
+243
View File
@@ -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
}
}
+158
View File
@@ -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
}
}