Codingstyle nullable
This commit is contained in:
+833
-883
File diff suppressed because it is too large
Load Diff
@@ -1,28 +1,26 @@
|
||||
namespace Swan
|
||||
{
|
||||
namespace Swan {
|
||||
/// <summary>
|
||||
/// Enumerates the possible causes of the DataReceived event occurring.
|
||||
/// </summary>
|
||||
public enum ConnectionDataReceivedTrigger {
|
||||
/// <summary>
|
||||
/// Enumerates the possible causes of the DataReceived event occurring.
|
||||
/// The trigger was a forceful flush of the buffer
|
||||
/// </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,
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
+222
-249
@@ -1,253 +1,226 @@
|
||||
namespace Swan.Net
|
||||
{
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Swan.Net {
|
||||
/// <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 Boolean _cancellationPending;
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0069:Verwerfbare Felder verwerfen", Justification = "<Ausstehend>")]
|
||||
private CancellationTokenSource? _cancelListening;
|
||||
private Task? _backgroundWorkerTask;
|
||||
private Boolean _hasDisposed;
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// TCP Listener manager with built-in events and asynchronous functionality.
|
||||
/// This networking component is typically used when writing server software.
|
||||
/// Occurs when a new connection requests a socket from the listener.
|
||||
/// Set Cancel = true to prevent the TCP client from being accepted.
|
||||
/// </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;
|
||||
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) {
|
||||
this.Id = Guid.NewGuid();
|
||||
this.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(Int32 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, Int32 listenPort) : this(new IPEndPoint(listenAddress, listenPort)) {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="ConnectionListener"/> class.
|
||||
/// </summary>
|
||||
~ConnectionListener() {
|
||||
this.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 Boolean IsListening => this._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(this._stateLock) {
|
||||
if(this._backgroundWorkerTask != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(this._cancellationPending) {
|
||||
throw new InvalidOperationException("Cancellation has already been requested. This listener is not reusable.");
|
||||
}
|
||||
|
||||
this._backgroundWorkerTask = this.DoWorkAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the listener from receiving new connections.
|
||||
/// This does not prevent the listener from .
|
||||
/// </summary>
|
||||
public void Stop() {
|
||||
lock(this._stateLock) {
|
||||
this._cancellationPending = true;
|
||||
this._listenerSocket?.Stop();
|
||||
this._cancelListening?.Cancel();
|
||||
this._backgroundWorkerTask?.Wait();
|
||||
this._backgroundWorkerTask = null;
|
||||
this._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() => this.LocalEndPoint.ToString();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() {
|
||||
this.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(Boolean disposing) {
|
||||
if(this._hasDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(disposing) {
|
||||
// Release managed resources
|
||||
this.Stop();
|
||||
}
|
||||
|
||||
this._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() {
|
||||
this._cancellationPending = false;
|
||||
this._listenerSocket = new TcpListener(this.LocalEndPoint);
|
||||
this._listenerSocket.Start();
|
||||
this._cancelListening = new CancellationTokenSource();
|
||||
|
||||
try {
|
||||
while(this._cancellationPending == false) {
|
||||
try {
|
||||
TcpClient client = await Task.Run(() => this._listenerSocket.AcceptTcpClientAsync(), this._cancelListening.Token).ConfigureAwait(false);
|
||||
ConnectionAcceptingEventArgs acceptingArgs = new ConnectionAcceptingEventArgs(client);
|
||||
OnConnectionAccepting(this, acceptingArgs);
|
||||
|
||||
if(acceptingArgs.Cancel) {
|
||||
client.Dispose();
|
||||
|
||||
#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
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
OnConnectionAccepted(this, new ConnectionAcceptedEventArgs(client));
|
||||
} catch(Exception ex) {
|
||||
OnConnectionFailure(this, new ConnectionFailureEventArgs(ex));
|
||||
}
|
||||
}
|
||||
|
||||
OnListenerStopped(this, new ConnectionListenerStoppedEventArgs(this.LocalEndPoint));
|
||||
} catch(ObjectDisposedException) {
|
||||
OnListenerStopped(this, new ConnectionListenerStoppedEventArgs(this.LocalEndPoint));
|
||||
} catch(Exception ex) {
|
||||
OnListenerStopped(this,
|
||||
new ConnectionListenerStoppedEventArgs(this.LocalEndPoint, this._cancellationPending ? null : ex));
|
||||
} finally {
|
||||
this._backgroundWorkerTask = null;
|
||||
this._cancellationPending = false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +1,96 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Swan.Net.Dns {
|
||||
/// <summary>
|
||||
/// DnsClient public interfaces.
|
||||
/// </summary>
|
||||
internal partial class DnsClient {
|
||||
public interface IDnsMessage {
|
||||
IList<DnsQuestion> Questions {
|
||||
get;
|
||||
}
|
||||
|
||||
Int32 Size {
|
||||
get;
|
||||
}
|
||||
Byte[] ToArray();
|
||||
}
|
||||
|
||||
public interface IDnsMessageEntry {
|
||||
DnsDomain Name {
|
||||
get;
|
||||
}
|
||||
DnsRecordType Type {
|
||||
get;
|
||||
}
|
||||
DnsRecordClass Class {
|
||||
get;
|
||||
}
|
||||
|
||||
Int32 Size {
|
||||
get;
|
||||
}
|
||||
Byte[] ToArray();
|
||||
}
|
||||
|
||||
public interface IDnsResourceRecord : IDnsMessageEntry {
|
||||
TimeSpan TimeToLive {
|
||||
get;
|
||||
}
|
||||
Int32 DataLength {
|
||||
get;
|
||||
}
|
||||
Byte[] Data {
|
||||
get;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IDnsRequest : IDnsMessage {
|
||||
Int32 Id {
|
||||
get; set;
|
||||
}
|
||||
DnsOperationCode OperationCode {
|
||||
get; set;
|
||||
}
|
||||
Boolean RecursionDesired {
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IDnsResponse : IDnsMessage {
|
||||
Int32 Id {
|
||||
get; set;
|
||||
}
|
||||
IList<IDnsResourceRecord> AnswerRecords {
|
||||
get;
|
||||
}
|
||||
IList<IDnsResourceRecord> AuthorityRecords {
|
||||
get;
|
||||
}
|
||||
IList<IDnsResourceRecord> AdditionalRecords {
|
||||
get;
|
||||
}
|
||||
Boolean IsRecursionAvailable {
|
||||
get; set;
|
||||
}
|
||||
Boolean IsAuthorativeServer {
|
||||
get; set;
|
||||
}
|
||||
Boolean IsTruncated {
|
||||
get; set;
|
||||
}
|
||||
DnsOperationCode OperationCode {
|
||||
get; set;
|
||||
}
|
||||
DnsResponseCode ResponseCode {
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IDnsRequestResolver {
|
||||
Task<DnsClientResponse> Request(DnsClientRequest request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+557
-680
File diff suppressed because it is too large
Load Diff
@@ -1,419 +1,344 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
using Swan.Formatters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Swan.Net.Dns {
|
||||
/// <summary>
|
||||
/// DnsClient public methods.
|
||||
/// </summary>
|
||||
internal partial class DnsClient {
|
||||
public abstract class DnsResourceRecordBase : IDnsResourceRecord {
|
||||
private readonly IDnsResourceRecord _record;
|
||||
|
||||
protected DnsResourceRecordBase(IDnsResourceRecord record) => this._record = record;
|
||||
|
||||
public DnsDomain Name => this._record.Name;
|
||||
|
||||
public DnsRecordType Type => this._record.Type;
|
||||
|
||||
public DnsRecordClass Class => this._record.Class;
|
||||
|
||||
public TimeSpan TimeToLive => this._record.TimeToLive;
|
||||
|
||||
public Int32 DataLength => this._record.DataLength;
|
||||
|
||||
public Byte[] Data => this._record.Data;
|
||||
|
||||
public Int32 Size => this._record.Size;
|
||||
|
||||
protected virtual String[] IncludedProperties => new[] { nameof(this.Name), nameof(this.Type), nameof(this.Class), nameof(this.TimeToLive), nameof(this.DataLength) };
|
||||
|
||||
public Byte[] ToArray() => this._record.ToArray();
|
||||
|
||||
public override String ToString() => Json.SerializeOnly(this, true, this.IncludedProperties);
|
||||
}
|
||||
|
||||
public class DnsResourceRecord : IDnsResourceRecord {
|
||||
public DnsResourceRecord(DnsDomain domain, Byte[] data, DnsRecordType type, DnsRecordClass klass = DnsRecordClass.IN, TimeSpan ttl = default) {
|
||||
this.Name = domain;
|
||||
this.Type = type;
|
||||
this.Class = klass;
|
||||
this.TimeToLive = ttl;
|
||||
this.Data = data;
|
||||
}
|
||||
|
||||
public DnsDomain Name {
|
||||
get;
|
||||
}
|
||||
|
||||
public DnsRecordType Type {
|
||||
get;
|
||||
}
|
||||
|
||||
public DnsRecordClass Class {
|
||||
get;
|
||||
}
|
||||
|
||||
public TimeSpan TimeToLive {
|
||||
get;
|
||||
}
|
||||
|
||||
public Int32 DataLength => this.Data.Length;
|
||||
|
||||
public Byte[] Data {
|
||||
get;
|
||||
}
|
||||
|
||||
public Int32 Size => this.Name.Size + Tail.SIZE + this.Data.Length;
|
||||
|
||||
public static DnsResourceRecord FromArray(Byte[] message, Int32 offset, out Int32 endOffset) {
|
||||
DnsDomain domain = DnsDomain.FromArray(message, offset, out offset);
|
||||
Tail tail = message.ToStruct<Tail>(offset, Tail.SIZE);
|
||||
|
||||
Byte[] data = new Byte[tail.DataLength];
|
||||
|
||||
offset += Tail.SIZE;
|
||||
Array.Copy(message, offset, data, 0, data.Length);
|
||||
|
||||
endOffset = offset + data.Length;
|
||||
|
||||
return new DnsResourceRecord(domain, data, tail.Type, tail.Class, tail.TimeToLive);
|
||||
}
|
||||
|
||||
public Byte[] ToArray() => new MemoryStream(this.Size).Append(this.Name.ToArray()).Append(new Tail() { Type = Type, Class = Class, TimeToLive = TimeToLive, DataLength = this.Data.Length, }.ToBytes()).Append(this.Data).ToArray();
|
||||
|
||||
public override String ToString() => Json.SerializeOnly(this, true, nameof(this.Name), nameof(this.Type), nameof(this.Class), nameof(this.TimeToLive), nameof(this.DataLength));
|
||||
|
||||
[StructEndianness(Endianness.Big)]
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 2)]
|
||||
private struct Tail {
|
||||
public const Int32 SIZE = 10;
|
||||
|
||||
private UInt16 type;
|
||||
private UInt16 klass;
|
||||
private UInt32 ttl;
|
||||
private UInt16 dataLength;
|
||||
|
||||
public DnsRecordType Type {
|
||||
get => (DnsRecordType)this.type;
|
||||
set => this.type = (UInt16)value;
|
||||
}
|
||||
|
||||
public DnsRecordClass Class {
|
||||
get => (DnsRecordClass)this.klass;
|
||||
set => this.klass = (UInt16)value;
|
||||
}
|
||||
|
||||
public TimeSpan TimeToLive {
|
||||
get => TimeSpan.FromSeconds(this.ttl);
|
||||
set => this.ttl = (UInt32)value.TotalSeconds;
|
||||
}
|
||||
|
||||
public Int32 DataLength {
|
||||
get => this.dataLength;
|
||||
set => this.dataLength = (UInt16)value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class DnsPointerResourceRecord : DnsResourceRecordBase {
|
||||
public DnsPointerResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) : base(record) => this.PointerDomainName = DnsDomain.FromArray(message, dataOffset);
|
||||
|
||||
public DnsDomain PointerDomainName {
|
||||
get;
|
||||
}
|
||||
|
||||
protected override String[] IncludedProperties {
|
||||
get {
|
||||
List<String> temp = new List<String>(base.IncludedProperties) { nameof(this.PointerDomainName) };
|
||||
return temp.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class DnsIPAddressResourceRecord : DnsResourceRecordBase {
|
||||
public DnsIPAddressResourceRecord(IDnsResourceRecord record) : base(record) => this.IPAddress = new IPAddress(this.Data);
|
||||
|
||||
public IPAddress IPAddress {
|
||||
get;
|
||||
}
|
||||
|
||||
protected override String[] IncludedProperties => new List<String>(base.IncludedProperties) { nameof(this.IPAddress) }.ToArray();
|
||||
}
|
||||
|
||||
public class DnsNameServerResourceRecord : DnsResourceRecordBase {
|
||||
public DnsNameServerResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) : base(record) => this.NSDomainName = DnsDomain.FromArray(message, dataOffset);
|
||||
|
||||
public DnsDomain NSDomainName {
|
||||
get;
|
||||
}
|
||||
|
||||
protected override String[] IncludedProperties => new List<String>(base.IncludedProperties) { nameof(this.NSDomainName) }.ToArray();
|
||||
}
|
||||
|
||||
public class DnsCanonicalNameResourceRecord : DnsResourceRecordBase {
|
||||
public DnsCanonicalNameResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) : base(record) => this.CanonicalDomainName = DnsDomain.FromArray(message, dataOffset);
|
||||
|
||||
public DnsDomain CanonicalDomainName {
|
||||
get;
|
||||
}
|
||||
|
||||
protected override String[] IncludedProperties => new List<String>(base.IncludedProperties) { nameof(this.CanonicalDomainName) }.ToArray();
|
||||
}
|
||||
|
||||
public class DnsMailExchangeResourceRecord : DnsResourceRecordBase {
|
||||
private const Int32 PreferenceSize = 2;
|
||||
|
||||
public DnsMailExchangeResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset)
|
||||
: base(record) {
|
||||
Byte[] preference = new Byte[PreferenceSize];
|
||||
Array.Copy(message, dataOffset, preference, 0, preference.Length);
|
||||
|
||||
if(BitConverter.IsLittleEndian) {
|
||||
Array.Reverse(preference);
|
||||
}
|
||||
|
||||
dataOffset += PreferenceSize;
|
||||
|
||||
this.Preference = BitConverter.ToUInt16(preference, 0);
|
||||
this.ExchangeDomainName = DnsDomain.FromArray(message, dataOffset);
|
||||
}
|
||||
|
||||
public Int32 Preference {
|
||||
get;
|
||||
}
|
||||
|
||||
public DnsDomain ExchangeDomainName {
|
||||
get;
|
||||
}
|
||||
|
||||
protected override String[] IncludedProperties => new List<String>(base.IncludedProperties)
|
||||
{
|
||||
nameof(this.Preference),
|
||||
nameof(this.ExchangeDomainName),
|
||||
}.ToArray();
|
||||
}
|
||||
|
||||
public class DnsStartOfAuthorityResourceRecord : DnsResourceRecordBase {
|
||||
public DnsStartOfAuthorityResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) : base(record) {
|
||||
this.MasterDomainName = DnsDomain.FromArray(message, dataOffset, out dataOffset);
|
||||
this.ResponsibleDomainName = DnsDomain.FromArray(message, dataOffset, out dataOffset);
|
||||
|
||||
Options tail = message.ToStruct<Options>(dataOffset, Options.SIZE);
|
||||
|
||||
this.SerialNumber = tail.SerialNumber;
|
||||
this.RefreshInterval = tail.RefreshInterval;
|
||||
this.RetryInterval = tail.RetryInterval;
|
||||
this.ExpireInterval = tail.ExpireInterval;
|
||||
this.MinimumTimeToLive = tail.MinimumTimeToLive;
|
||||
}
|
||||
|
||||
public DnsStartOfAuthorityResourceRecord(DnsDomain domain, DnsDomain master, DnsDomain responsible, Int64 serial, TimeSpan refresh, TimeSpan retry, TimeSpan expire, TimeSpan minTtl, TimeSpan ttl = default)
|
||||
: base(Create(domain, master, responsible, serial, refresh, retry, expire, minTtl, ttl)) {
|
||||
this.MasterDomainName = master;
|
||||
this.ResponsibleDomainName = responsible;
|
||||
|
||||
this.SerialNumber = serial;
|
||||
this.RefreshInterval = refresh;
|
||||
this.RetryInterval = retry;
|
||||
this.ExpireInterval = expire;
|
||||
this.MinimumTimeToLive = minTtl;
|
||||
}
|
||||
|
||||
public DnsDomain MasterDomainName {
|
||||
get;
|
||||
}
|
||||
|
||||
public DnsDomain ResponsibleDomainName {
|
||||
get;
|
||||
}
|
||||
|
||||
public Int64 SerialNumber {
|
||||
get;
|
||||
}
|
||||
|
||||
public TimeSpan RefreshInterval {
|
||||
get;
|
||||
}
|
||||
|
||||
public TimeSpan RetryInterval {
|
||||
get;
|
||||
}
|
||||
|
||||
public TimeSpan ExpireInterval {
|
||||
get;
|
||||
}
|
||||
|
||||
public TimeSpan MinimumTimeToLive {
|
||||
get;
|
||||
}
|
||||
|
||||
protected override String[] IncludedProperties => new List<String>(base.IncludedProperties)
|
||||
{
|
||||
nameof(this.MasterDomainName),
|
||||
nameof(this.ResponsibleDomainName),
|
||||
nameof(this.SerialNumber),
|
||||
}.ToArray();
|
||||
|
||||
private static IDnsResourceRecord Create(DnsDomain domain, DnsDomain master, DnsDomain responsible, Int64 serial, TimeSpan refresh, TimeSpan retry, TimeSpan expire, TimeSpan minTtl, TimeSpan ttl) {
|
||||
MemoryStream data = new MemoryStream(Options.SIZE + master.Size + responsible.Size);
|
||||
Options tail = new Options {
|
||||
SerialNumber = serial,
|
||||
RefreshInterval = refresh,
|
||||
RetryInterval = retry,
|
||||
ExpireInterval = expire,
|
||||
MinimumTimeToLive = minTtl,
|
||||
};
|
||||
|
||||
_ = data.Append(master.ToArray()).Append(responsible.ToArray()).Append(tail.ToBytes());
|
||||
|
||||
return new DnsResourceRecord(domain, data.ToArray(), DnsRecordType.SOA, DnsRecordClass.IN, ttl);
|
||||
}
|
||||
|
||||
[StructEndianness(Endianness.Big)]
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
public struct Options {
|
||||
public const Int32 SIZE = 20;
|
||||
|
||||
private UInt32 serialNumber;
|
||||
private UInt32 refreshInterval;
|
||||
private UInt32 retryInterval;
|
||||
private UInt32 expireInterval;
|
||||
private UInt32 ttl;
|
||||
|
||||
public Int64 SerialNumber {
|
||||
get => this.serialNumber;
|
||||
set => this.serialNumber = (UInt32)value;
|
||||
}
|
||||
|
||||
public TimeSpan RefreshInterval {
|
||||
get => TimeSpan.FromSeconds(this.refreshInterval);
|
||||
set => this.refreshInterval = (UInt32)value.TotalSeconds;
|
||||
}
|
||||
|
||||
public TimeSpan RetryInterval {
|
||||
get => TimeSpan.FromSeconds(this.retryInterval);
|
||||
set => this.retryInterval = (UInt32)value.TotalSeconds;
|
||||
}
|
||||
|
||||
public TimeSpan ExpireInterval {
|
||||
get => TimeSpan.FromSeconds(this.expireInterval);
|
||||
set => this.expireInterval = (UInt32)value.TotalSeconds;
|
||||
}
|
||||
|
||||
public TimeSpan MinimumTimeToLive {
|
||||
get => TimeSpan.FromSeconds(this.ttl);
|
||||
set => this.ttl = (UInt32)value.TotalSeconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class DnsResourceRecordFactory {
|
||||
public static IList<IDnsResourceRecord> GetAllFromArray(Byte[] message, Int32 offset, Int32 count, out Int32 endOffset) {
|
||||
List<IDnsResourceRecord> result = new List<IDnsResourceRecord>(count);
|
||||
|
||||
for(Int32 i = 0; i < count; i++) {
|
||||
result.Add(GetFromArray(message, offset, out offset));
|
||||
}
|
||||
|
||||
endOffset = offset;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static IDnsResourceRecord GetFromArray(Byte[] message, Int32 offset, out Int32 endOffset) {
|
||||
DnsResourceRecord record = DnsResourceRecord.FromArray(message, offset, out endOffset);
|
||||
Int32 dataOffset = endOffset - record.DataLength;
|
||||
|
||||
return record.Type switch
|
||||
{
|
||||
DnsRecordType.A => (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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+173
-214
@@ -1,215 +1,174 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
using Swan.Formatters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Swan.Net.Dns {
|
||||
/// <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) {
|
||||
this.Request = request;
|
||||
|
||||
this._message = message;
|
||||
this._response = response;
|
||||
}
|
||||
|
||||
public DnsClientRequest Request {
|
||||
get;
|
||||
}
|
||||
|
||||
public Int32 Id {
|
||||
get => this._response.Id;
|
||||
set {
|
||||
}
|
||||
}
|
||||
|
||||
public IList<IDnsResourceRecord> AnswerRecords => this._response.AnswerRecords;
|
||||
|
||||
public IList<IDnsResourceRecord> AuthorityRecords => new ReadOnlyCollection<IDnsResourceRecord>(this._response.AuthorityRecords);
|
||||
|
||||
public IList<IDnsResourceRecord> AdditionalRecords => new ReadOnlyCollection<IDnsResourceRecord>(this._response.AdditionalRecords);
|
||||
|
||||
public Boolean IsRecursionAvailable {
|
||||
get => this._response.IsRecursionAvailable;
|
||||
set {
|
||||
}
|
||||
}
|
||||
|
||||
public Boolean IsAuthorativeServer {
|
||||
get => this._response.IsAuthorativeServer;
|
||||
set {
|
||||
}
|
||||
}
|
||||
|
||||
public Boolean IsTruncated {
|
||||
get => this._response.IsTruncated;
|
||||
set {
|
||||
}
|
||||
}
|
||||
|
||||
public DnsOperationCode OperationCode {
|
||||
get => this._response.OperationCode;
|
||||
set {
|
||||
}
|
||||
}
|
||||
|
||||
public DnsResponseCode ResponseCode {
|
||||
get => this._response.ResponseCode;
|
||||
set {
|
||||
}
|
||||
}
|
||||
|
||||
public IList<DnsQuestion> Questions => new ReadOnlyCollection<DnsQuestion>(this._response.Questions);
|
||||
|
||||
public Int32 Size => this._message.Length;
|
||||
|
||||
public Byte[] ToArray() => this._message;
|
||||
|
||||
public override String ToString() => this._response.ToString();
|
||||
}
|
||||
|
||||
public class DnsResponse : IDnsResponse {
|
||||
private DnsHeader _header;
|
||||
|
||||
public DnsResponse(DnsHeader header, IList<DnsQuestion> questions, IList<IDnsResourceRecord> answers, IList<IDnsResourceRecord> authority, IList<IDnsResourceRecord> additional) {
|
||||
this._header = header;
|
||||
this.Questions = questions;
|
||||
this.AnswerRecords = answers;
|
||||
this.AuthorityRecords = authority;
|
||||
this.AdditionalRecords = additional;
|
||||
}
|
||||
|
||||
public IList<DnsQuestion> Questions {
|
||||
get;
|
||||
}
|
||||
|
||||
public IList<IDnsResourceRecord> AnswerRecords {
|
||||
get;
|
||||
}
|
||||
|
||||
public IList<IDnsResourceRecord> AuthorityRecords {
|
||||
get;
|
||||
}
|
||||
|
||||
public IList<IDnsResourceRecord> AdditionalRecords {
|
||||
get;
|
||||
}
|
||||
|
||||
public Int32 Id {
|
||||
get => this._header.Id;
|
||||
set => this._header.Id = value;
|
||||
}
|
||||
|
||||
public Boolean IsRecursionAvailable {
|
||||
get => this._header.RecursionAvailable;
|
||||
set => this._header.RecursionAvailable = value;
|
||||
}
|
||||
|
||||
public Boolean IsAuthorativeServer {
|
||||
get => this._header.AuthorativeServer;
|
||||
set => this._header.AuthorativeServer = value;
|
||||
}
|
||||
|
||||
public Boolean IsTruncated {
|
||||
get => this._header.Truncated;
|
||||
set => this._header.Truncated = value;
|
||||
}
|
||||
|
||||
public DnsOperationCode OperationCode {
|
||||
get => this._header.OperationCode;
|
||||
set => this._header.OperationCode = value;
|
||||
}
|
||||
|
||||
public DnsResponseCode ResponseCode {
|
||||
get => this._header.ResponseCode;
|
||||
set => this._header.ResponseCode = value;
|
||||
}
|
||||
|
||||
public Int32 Size => this._header.Size + this.Questions.Sum(q => q.Size) + this.AnswerRecords.Sum(a => a.Size) + this.AuthorityRecords.Sum(a => a.Size) + this.AdditionalRecords.Sum(a => a.Size);
|
||||
|
||||
public static DnsResponse FromArray(Byte[] message) {
|
||||
DnsHeader header = DnsHeader.FromArray(message);
|
||||
Int32 offset = header.Size;
|
||||
|
||||
if(!header.Response || header.QuestionCount == 0) {
|
||||
throw new ArgumentException("Invalid response message");
|
||||
}
|
||||
|
||||
return header.Truncated
|
||||
? new DnsResponse(header, DnsQuestion.GetAllFromArray(message, offset, header.QuestionCount), new List<IDnsResourceRecord>(), new List<IDnsResourceRecord>(), new List<IDnsResourceRecord>())
|
||||
: new DnsResponse(header, DnsQuestion.GetAllFromArray(message, offset, header.QuestionCount, out offset), DnsResourceRecordFactory.GetAllFromArray(message, offset, header.AnswerRecordCount, out offset), DnsResourceRecordFactory.GetAllFromArray(message, offset, header.AuthorityRecordCount, out offset), DnsResourceRecordFactory.GetAllFromArray(message, offset, header.AdditionalRecordCount, out _));
|
||||
}
|
||||
|
||||
public Byte[] ToArray() {
|
||||
this.UpdateHeader();
|
||||
MemoryStream result = new MemoryStream(this.Size);
|
||||
|
||||
_ = result.Append(this._header.ToArray()).Append(this.Questions.Select(q => q.ToArray())).Append(this.AnswerRecords.Select(a => a.ToArray())).Append(this.AuthorityRecords.Select(a => a.ToArray())).Append(this.AdditionalRecords.Select(a => a.ToArray()));
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
public override String ToString() {
|
||||
this.UpdateHeader();
|
||||
|
||||
return Json.SerializeOnly(this, true, nameof(this.Questions), nameof(this.AnswerRecords), nameof(this.AuthorityRecords), nameof(this.AdditionalRecords));
|
||||
}
|
||||
|
||||
private void UpdateHeader() {
|
||||
this._header.QuestionCount = this.Questions.Count;
|
||||
this._header.AnswerRecordCount = this.AnswerRecords.Count;
|
||||
this._header.AuthorityRecordCount = this.AuthorityRecords.Count;
|
||||
this._header.AdditionalRecordCount = this.AdditionalRecords.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+64
-78
@@ -1,79 +1,65 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
#nullable enable
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Swan.Net.Dns {
|
||||
/// <summary>
|
||||
/// DnsClient public methods.
|
||||
/// </summary>
|
||||
internal partial class DnsClient {
|
||||
private readonly IPEndPoint _dns;
|
||||
private readonly IDnsRequestResolver _resolver;
|
||||
|
||||
public DnsClient(IPEndPoint dns, IDnsRequestResolver? resolver = null) {
|
||||
this._dns = dns;
|
||||
this._resolver = resolver ?? new DnsUdpRequestResolver(new DnsTcpRequestResolver());
|
||||
}
|
||||
|
||||
public DnsClient(IPAddress ip, Int32 port = Network.DnsDefaultPort, IDnsRequestResolver? resolver = null) : this(new IPEndPoint(ip, port), resolver) {
|
||||
}
|
||||
|
||||
public DnsClientRequest Create(IDnsRequest? request = null) => new DnsClientRequest(this._dns, request, this._resolver);
|
||||
|
||||
public 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);
|
||||
}
|
||||
|
||||
DnsClientResponse response = await this.Resolve(domain, type).ConfigureAwait(false);
|
||||
List<IPAddress> 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));
|
||||
}
|
||||
|
||||
DnsClientResponse response = await this.Resolve(DnsDomain.PointerName(ip), DnsRecordType.PTR);
|
||||
IDnsResourceRecord 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) => this.Resolve(new DnsDomain(domain), type);
|
||||
|
||||
public Task<DnsClientResponse> Resolve(DnsDomain domain, DnsRecordType type) {
|
||||
DnsClientRequest request = this.Create();
|
||||
DnsQuestion question = new DnsQuestion(domain, type);
|
||||
|
||||
request.Questions.Add(question);
|
||||
request.OperationCode = DnsOperationCode.Query;
|
||||
request.RecursionDesired = true;
|
||||
|
||||
return request.Resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,28 @@
|
||||
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}";
|
||||
}
|
||||
#nullable enable
|
||||
using System;
|
||||
|
||||
namespace Swan.Net.Dns {
|
||||
/// <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) => this.Response = response;
|
||||
|
||||
internal DnsClient.IDnsResponse? Response {
|
||||
get;
|
||||
}
|
||||
|
||||
private static String Format(DnsClient.IDnsResponse response) => $"Invalid response received with code {response.ResponseCode}";
|
||||
}
|
||||
}
|
||||
|
||||
+127
-120
@@ -1,123 +1,130 @@
|
||||
namespace Swan.Net.Dns
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
|
||||
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>
|
||||
/// Represents a response from a DNS server.
|
||||
/// Initializes a new instance of the <see cref="DnsQueryResult"/> class.
|
||||
/// </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;
|
||||
}
|
||||
/// <param name="response">The response.</param>
|
||||
internal DnsQueryResult(DnsClient.IDnsResponse response) : this() {
|
||||
this.Id = response.Id;
|
||||
this.IsAuthoritativeServer = response.IsAuthorativeServer;
|
||||
this.IsRecursionAvailable = response.IsRecursionAvailable;
|
||||
this.IsTruncated = response.IsTruncated;
|
||||
this.OperationCode = response.OperationCode;
|
||||
this.ResponseCode = response.ResponseCode;
|
||||
|
||||
if(response.AnswerRecords != null) {
|
||||
foreach(DnsClient.IDnsResourceRecord record in response.AnswerRecords) {
|
||||
this.AnswerRecords.Add(new DnsRecord(record));
|
||||
}
|
||||
}
|
||||
|
||||
if(response.AuthorityRecords != null) {
|
||||
foreach(DnsClient.IDnsResourceRecord record in response.AuthorityRecords) {
|
||||
this.AuthorityRecords.Add(new DnsRecord(record));
|
||||
}
|
||||
}
|
||||
|
||||
if(response.AdditionalRecords != null) {
|
||||
foreach(DnsClient.IDnsResourceRecord record in response.AdditionalRecords) {
|
||||
this.AdditionalRecords.Add(new DnsRecord(record));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DnsQueryResult() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The identifier.
|
||||
/// </value>
|
||||
public System.Int32 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 System.Boolean 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 System.Boolean 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 System.Boolean 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 => this._mAnswerRecords;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the additional records.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The additional records.
|
||||
/// </value>
|
||||
public IList<DnsRecord> AdditionalRecords => this._mAdditionalRecords;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authority records.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The authority records.
|
||||
/// </value>
|
||||
public IList<DnsRecord> AuthorityRecords => this._mAuthorityRecords;
|
||||
}
|
||||
}
|
||||
|
||||
+236
-205
@@ -1,208 +1,239 @@
|
||||
namespace Swan.Net.Dns
|
||||
{
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
|
||||
namespace Swan.Net.Dns {
|
||||
/// <summary>
|
||||
/// Represents a DNS record entry.
|
||||
/// </summary>
|
||||
public class DnsRecord {
|
||||
/// <summary>
|
||||
/// Represents a DNS record entry.
|
||||
/// Initializes a new instance of the <see cref="DnsRecord"/> class.
|
||||
/// </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; }
|
||||
}
|
||||
/// <param name="record">The record.</param>
|
||||
internal DnsRecord(DnsClient.IDnsResourceRecord record) : this() {
|
||||
this.Name = record.Name.ToString();
|
||||
this.Type = record.Type;
|
||||
this.Class = record.Class;
|
||||
this.TimeToLive = record.TimeToLive;
|
||||
this.Data = record.Data;
|
||||
|
||||
// PTR
|
||||
this.PointerDomainName = (record as DnsClient.DnsPointerResourceRecord)?.PointerDomainName?.ToString();
|
||||
|
||||
// A
|
||||
this.IPAddress = (record as DnsClient.DnsIPAddressResourceRecord)?.IPAddress;
|
||||
|
||||
// NS
|
||||
this.NameServerDomainName = (record as DnsClient.DnsNameServerResourceRecord)?.NSDomainName?.ToString();
|
||||
|
||||
// CNAME
|
||||
this.CanonicalDomainName = (record as DnsClient.DnsCanonicalNameResourceRecord)?.CanonicalDomainName.ToString();
|
||||
|
||||
// MX
|
||||
this.MailExchangerDomainName = (record as DnsClient.DnsMailExchangeResourceRecord)?.ExchangeDomainName.ToString();
|
||||
this.MailExchangerPreference = (record as DnsClient.DnsMailExchangeResourceRecord)?.Preference;
|
||||
|
||||
// SOA
|
||||
this.SoaMasterDomainName = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.MasterDomainName.ToString();
|
||||
this.SoaResponsibleDomainName = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.ResponsibleDomainName.ToString();
|
||||
this.SoaSerialNumber = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.SerialNumber;
|
||||
this.SoaRefreshInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.RefreshInterval;
|
||||
this.SoaRetryInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.RetryInterval;
|
||||
this.SoaExpireInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.ExpireInterval;
|
||||
this.SoaMinimumTimeToLive = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.MinimumTimeToLive;
|
||||
}
|
||||
|
||||
private DnsRecord() {
|
||||
// placeholder
|
||||
}
|
||||
|
||||
/// <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 => this.Data == null ? String.Empty : Encoding.ASCII.GetString(this.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 Int32? 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 Int64? 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+161
-166
@@ -1,172 +1,167 @@
|
||||
// ReSharper disable InconsistentNaming
|
||||
namespace Swan.Net.Dns
|
||||
{
|
||||
namespace Swan.Net.Dns {
|
||||
/// <summary>
|
||||
/// Enumerates the different DNS record types.
|
||||
/// </summary>
|
||||
public enum DnsRecordType {
|
||||
/// <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,
|
||||
}
|
||||
|
||||
/// A records
|
||||
/// </summary>
|
||||
A = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the different DNS record classes.
|
||||
/// </summary>
|
||||
public enum DnsRecordClass
|
||||
{
|
||||
/// <summary>
|
||||
/// IN records
|
||||
/// </summary>
|
||||
IN = 1,
|
||||
|
||||
/// <summary>
|
||||
/// ANY records
|
||||
/// </summary>
|
||||
ANY = 255,
|
||||
}
|
||||
|
||||
/// Nameserver records
|
||||
/// </summary>
|
||||
NS = 2,
|
||||
|
||||
/// <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,
|
||||
}
|
||||
|
||||
/// CNAME records
|
||||
/// </summary>
|
||||
CNAME = 5,
|
||||
|
||||
/// <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,
|
||||
}
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,158 +1,157 @@
|
||||
namespace Swan.Net
|
||||
{
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Swan.Net {
|
||||
/// <summary>
|
||||
/// The event arguments for when connections are accepted.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.EventArgs" />
|
||||
public class ConnectionAcceptedEventArgs : EventArgs {
|
||||
/// <summary>
|
||||
/// The event arguments for when connections are accepted.
|
||||
/// Initializes a new instance of the <see cref="ConnectionAcceptedEventArgs" /> class.
|
||||
/// </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; }
|
||||
}
|
||||
|
||||
/// <param name="client">The client.</param>
|
||||
/// <exception cref="ArgumentNullException">client.</exception>
|
||||
public ConnectionAcceptedEventArgs(TcpClient client) => this.Client = client ?? throw new ArgumentNullException(nameof(client));
|
||||
|
||||
/// <summary>
|
||||
/// Occurs before a connection is accepted. Set the Cancel property to true to prevent the connection from being accepted.
|
||||
/// Gets the client.
|
||||
/// </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; }
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Event arguments for when a server listener is started.
|
||||
/// Initializes a new instance of the <see cref="ConnectionAcceptingEventArgs"/> class.
|
||||
/// </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; }
|
||||
}
|
||||
|
||||
/// <param name="client">The client.</param>
|
||||
public ConnectionAcceptingEventArgs(TcpClient client) : base(client) {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event arguments for when a server listener fails to start.
|
||||
/// Setting Cancel to true rejects the new TcpClient.
|
||||
/// </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; }
|
||||
}
|
||||
|
||||
/// <value>
|
||||
/// <c>true</c> if cancel; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public Boolean Cancel {
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event arguments for when a server listener is started.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.EventArgs" />
|
||||
public class ConnectionListenerStartedEventArgs : EventArgs {
|
||||
/// <summary>
|
||||
/// Event arguments for when a server listener stopped.
|
||||
/// Initializes a new instance of the <see cref="ConnectionListenerStartedEventArgs" /> class.
|
||||
/// </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; }
|
||||
}
|
||||
/// <param name="listenerEndPoint">The listener end point.</param>
|
||||
/// <exception cref="ArgumentNullException">listenerEndPoint.</exception>
|
||||
public ConnectionListenerStartedEventArgs(IPEndPoint listenerEndPoint) => this.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) {
|
||||
this.EndPoint = listenerEndPoint ?? throw new ArgumentNullException(nameof(listenerEndPoint));
|
||||
this.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) {
|
||||
this.EndPoint = listenerEndPoint ?? throw new ArgumentNullException(nameof(listenerEndPoint));
|
||||
this.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+79
-79
@@ -1,84 +1,84 @@
|
||||
namespace Swan.Net
|
||||
{
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Swan.Net {
|
||||
/// <summary>
|
||||
/// The event arguments for connection failure events.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.EventArgs" />
|
||||
public class ConnectionFailureEventArgs : EventArgs {
|
||||
/// <summary>
|
||||
/// The event arguments for connection failure events.
|
||||
/// Initializes a new instance of the <see cref="ConnectionFailureEventArgs"/> class.
|
||||
/// </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; }
|
||||
}
|
||||
|
||||
/// <param name="ex">The ex.</param>
|
||||
public ConnectionFailureEventArgs(Exception ex) => this.Error = ex;
|
||||
|
||||
/// <summary>
|
||||
/// Event arguments for when data is received.
|
||||
/// Gets the error.
|
||||
/// </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));
|
||||
}
|
||||
/// <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, Boolean moreAvailable) {
|
||||
this.Buffer = buffer ?? throw new ArgumentNullException(nameof(buffer));
|
||||
this.Trigger = trigger;
|
||||
this.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 Boolean 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(this.Buffer).TrimEnd('\r', '\n') ?? throw new ArgumentNullException(nameof(encoding));
|
||||
}
|
||||
}
|
||||
|
||||
+310
-415
@@ -1,418 +1,313 @@
|
||||
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;
|
||||
|
||||
#nullable enable
|
||||
using Swan.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;
|
||||
|
||||
namespace Swan.Net {
|
||||
/// <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>
|
||||
/// Represents a HttpClient with extended methods to use with JSON payloads
|
||||
/// and bearer tokens authentication.
|
||||
/// Post a object as JSON with optional authorization token.
|
||||
/// </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);
|
||||
}
|
||||
}
|
||||
/// <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) where T : notnull {
|
||||
String 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) {
|
||||
String 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) where T : notnull {
|
||||
String 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) {
|
||||
Object 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) {
|
||||
HttpContent 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) where T : notnull {
|
||||
String 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) where T : notnull {
|
||||
String 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) {
|
||||
HttpContent 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
|
||||
String content = $"grant_type=password&username={username}&password={password}";
|
||||
using StringContent requestContent = new StringContent(content, Encoding.UTF8, FormType);
|
||||
HttpResponseMessage response = await HttpClient.PostAsync(requestUri, requestContent, ct).ConfigureAwait(false);
|
||||
|
||||
if(!response.IsSuccessStatusCode) {
|
||||
throw new SecurityException($"Error Authenticating. Status code: {response.StatusCode}.");
|
||||
}
|
||||
|
||||
String 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) where T : notnull => 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 HttpResponseMessage response = await GetResponse(requestUri, authorization, null, payload, method, ct).ConfigureAwait(false);
|
||||
if(!response.IsSuccessStatusCode) {
|
||||
throw new JsonRequestException(
|
||||
$"Error {method} JSON",
|
||||
(Int32)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) {
|
||||
HttpResponseMessage response = await GetResponse(uri, authorization, headers, ct: ct).ConfigureAwait(false);
|
||||
|
||||
return response.IsSuccessStatusCode ? response.Content : throw new JsonRequestException("Error GET", (Int32)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 HttpRequestMessage requestMessage = new HttpRequestMessage(method ?? HttpMethod.Get, uri);
|
||||
|
||||
if(!String.IsNullOrWhiteSpace(authorization)) {
|
||||
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authorization);
|
||||
}
|
||||
|
||||
if(headers != null) {
|
||||
foreach(KeyValuePair<String, IEnumerable<String>> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,44 @@
|
||||
namespace Swan.Net
|
||||
{
|
||||
using System;
|
||||
|
||||
using System;
|
||||
|
||||
namespace Swan.Net {
|
||||
/// <summary>
|
||||
/// Represents errors that occurs requesting a JSON file through HTTP.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.Exception" />
|
||||
[Serializable]
|
||||
public class JsonRequestException : Exception {
|
||||
/// <summary>
|
||||
/// Represents errors that occurs requesting a JSON file through HTTP.
|
||||
/// Initializes a new instance of the <see cref="JsonRequestException"/> class.
|
||||
/// </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();
|
||||
}
|
||||
/// <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, Int32 httpErrorCode = 500, String errorContent = null) : base(message) {
|
||||
this.HttpErrorCode = httpErrorCode;
|
||||
this.HttpErrorContent = errorContent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the HTTP error code.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The HTTP error code.
|
||||
/// </value>
|
||||
public Int32 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(this.HttpErrorContent) ? $"HTTP Response Status Code {this.HttpErrorCode} Error Message: {this.HttpErrorContent}" : base.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
+284
-323
@@ -1,328 +1,289 @@
|
||||
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;
|
||||
|
||||
using Swan.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;
|
||||
|
||||
namespace Swan.Net {
|
||||
/// <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>
|
||||
/// Provides miscellaneous network utilities such as a Public IP finder,
|
||||
/// a DNS client to query DNS records of any kind, and an NTP client.
|
||||
/// The DNS default port.
|
||||
/// </summary>
|
||||
public static class Network
|
||||
{
|
||||
/// <summary>
|
||||
/// The DNS default port.
|
||||
/// </summary>
|
||||
public const int DnsDefaultPort = 53;
|
||||
public const Int32 DnsDefaultPort = 53;
|
||||
|
||||
/// <summary>
|
||||
/// The NTP default port.
|
||||
/// </summary>
|
||||
public const Int32 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
|
||||
IPAddress zeroConf = new IPAddress(0);
|
||||
|
||||
NetworkInterface[] adapters = NetworkInterface.GetAllNetworkInterfaces().Where(network => network.OperationalStatus == OperationalStatus.Up && network.NetworkInterfaceType != NetworkInterfaceType.Unknown && network.NetworkInterfaceType != NetworkInterfaceType.Loopback).ToArray();
|
||||
|
||||
Dictionary<NetworkInterface, IPInterfaceProperties> result = new Dictionary<NetworkInterface, IPInterfaceProperties>();
|
||||
|
||||
foreach(NetworkInterface adapter in adapters) {
|
||||
IPInterfaceProperties properties = adapter.GetIPProperties();
|
||||
if(properties == null || properties.GatewayAddresses.Count == 0 || properties.GatewayAddresses.All(gateway => Equals(gateway.Address, zeroConf)) || properties.UnicastAddresses.Count == 0 || properties.GatewayAddresses.All(address => Equals(address.Address, zeroConf)) || properties.UnicastAddresses.Any(a => a.Address.AddressFamily == AddressFamily.InterNetwork) == false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result[adapter] = properties;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <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(Boolean 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, Boolean skipTypeFilter = false, Boolean includeLoopback = false) {
|
||||
List<IPAddress> addressList = new List<IPAddress>();
|
||||
NetworkInterface[] interfaces = NetworkInterface.GetAllNetworkInterfaces()
|
||||
.Where(ni => (skipTypeFilter || ni.NetworkInterfaceType == interfaceType) && ni.OperationalStatus == OperationalStatus.Up).ToArray();
|
||||
|
||||
foreach(NetworkInterface networkInterface in interfaces) {
|
||||
IPInterfaceProperties properties = networkInterface.GetIPProperties();
|
||||
|
||||
if(properties.GatewayAddresses.All(g => g.Address.AddressFamily != AddressFamily.InterNetwork)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
addressList.AddRange(properties.UnicastAddresses.Where(i => i.Address.AddressFamily == AddressFamily.InterNetwork).Select(i => i.Address));
|
||||
}
|
||||
|
||||
if(includeLoopback || interfaceType == NetworkInterfaceType.Loopback) {
|
||||
addressList.Add(IPAddress.Loopback);
|
||||
}
|
||||
|
||||
return addressList.ToArray();
|
||||
}
|
||||
|
||||
/// <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 HttpClient client = new HttpClient();
|
||||
HttpResponseMessage 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) {
|
||||
IPAddress 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, Int32 port) {
|
||||
if(fqdn == null) {
|
||||
throw new ArgumentNullException(nameof(fqdn));
|
||||
}
|
||||
|
||||
if(fqdn.IndexOf(".", StringComparison.Ordinal) == -1) {
|
||||
fqdn += "." + IPGlobalProperties.GetIPGlobalProperties().DomainName;
|
||||
}
|
||||
|
||||
while(true) {
|
||||
if(!fqdn.EndsWith(".", StringComparison.OrdinalIgnoreCase)) {
|
||||
break;
|
||||
}
|
||||
|
||||
fqdn = fqdn[0..^1];
|
||||
}
|
||||
|
||||
DnsClient client = new DnsClient(dnsServer, port);
|
||||
IList<IPAddress> 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, Int32 port) {
|
||||
DnsClient 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) {
|
||||
DnsClient 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, Int32 port) {
|
||||
if(query == null) {
|
||||
throw new ArgumentNullException(nameof(query));
|
||||
}
|
||||
|
||||
DnsClient client = new DnsClient(dnsServer, port);
|
||||
DnsClient.DnsClientResponse 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, Int32 port = NtpDefaultPort) {
|
||||
if(ntpServerAddress == null) {
|
||||
throw new ArgumentNullException(nameof(ntpServerAddress));
|
||||
}
|
||||
|
||||
// NTP message size - 16 bytes of the digest (RFC 2030)
|
||||
Byte[] ntpData = new Byte[48];
|
||||
|
||||
// Setting the Leap Indicator, Version Number and Mode values
|
||||
ntpData[0] = 0x1B; // LI = 0 (no warning), VN = 3 (IPv4 only), Mode = 3 (Client Mode)
|
||||
|
||||
// The UDP port number assigned to NTP is 123
|
||||
IPEndPoint endPoint = new IPEndPoint(ntpServerAddress, port);
|
||||
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The NTP default port.
|
||||
/// </summary>
|
||||
public const int NtpDefaultPort = 123;
|
||||
await socket.ConnectAsync(endPoint).ConfigureAwait(false);
|
||||
|
||||
/// <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
|
||||
}
|
||||
|
||||
socket.ReceiveTimeout = 3000; // Stops code hang if NTP is blocked
|
||||
_ = socket.Send(ntpData);
|
||||
_ = socket.Receive(ntpData);
|
||||
socket.Dispose();
|
||||
|
||||
// Offset to get to the "Transmit Timestamp" field (time at which the reply
|
||||
// departed the server for the client, in 64-bit timestamp format."
|
||||
const Byte serverReplyTime = 40;
|
||||
|
||||
// Get the seconds part
|
||||
UInt64 intPart = BitConverter.ToUInt32(ntpData, serverReplyTime);
|
||||
|
||||
// Get the seconds fraction
|
||||
UInt64 fractPart = BitConverter.ToUInt32(ntpData, serverReplyTime + 4);
|
||||
|
||||
// Convert From big-endian to little-endian to match the platform
|
||||
if(BitConverter.IsLittleEndian) {
|
||||
intPart = intPart.SwapEndianness();
|
||||
fractPart = intPart.SwapEndianness();
|
||||
}
|
||||
|
||||
UInt64 milliseconds = intPart * 1000 + fractPart * 1000 / 0x100000000L;
|
||||
|
||||
// The time is given in UTC
|
||||
return new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds((Int64)milliseconds);
|
||||
}
|
||||
|
||||
/// <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", Int32 port = NtpDefaultPort) {
|
||||
IPAddress[] addresses = await GetDnsHostEntryAsync(ntpServerName).ConfigureAwait(false);
|
||||
return await GetNetworkTimeUtcAsync(addresses.First(), port).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
+154
-158
@@ -1,166 +1,162 @@
|
||||
// ReSharper disable InconsistentNaming
|
||||
namespace Swan.Net.Smtp
|
||||
{
|
||||
namespace Swan.Net.Smtp {
|
||||
/// <summary>
|
||||
/// Enumerates all of the well-known SMTP command names.
|
||||
/// </summary>
|
||||
public enum SmtpCommandNames {
|
||||
/// <summary>
|
||||
/// Enumerates all of the well-known SMTP command names.
|
||||
/// An unknown command
|
||||
/// </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,
|
||||
}
|
||||
|
||||
Unknown,
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the reply code severities.
|
||||
/// The helo command
|
||||
/// </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,
|
||||
}
|
||||
|
||||
HELO,
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the reply code categories.
|
||||
/// The ehlo command
|
||||
/// </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,
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
+367
-385
@@ -1,388 +1,370 @@
|
||||
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;
|
||||
|
||||
#nullable enable
|
||||
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;
|
||||
|
||||
namespace Swan.Net.Smtp {
|
||||
/// <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>
|
||||
/// Represents a basic SMTP client that is capable of submitting messages to an SMTP server.
|
||||
/// Initializes a new instance of the <see cref="SmtpClient" /> class.
|
||||
/// </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();
|
||||
}
|
||||
}
|
||||
}
|
||||
/// <param name="host">The host.</param>
|
||||
/// <param name="port">The port.</param>
|
||||
/// <exception cref="ArgumentNullException">host.</exception>
|
||||
public SmtpClient(String host, Int32 port) {
|
||||
this.Host = host ?? throw new ArgumentNullException(nameof(host));
|
||||
this.Port = port;
|
||||
this.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 Int32 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 Boolean 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>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0067:Objekte verwerfen, bevor Bereich verloren geht", Justification = "<Ausstehend>")]
|
||||
public Task SendMailAsync(MailMessage message, String? sessionId = null, RemoteCertificateValidationCallback? callback = null, CancellationToken cancellationToken = default) {
|
||||
if(message == null) {
|
||||
throw new ArgumentNullException(nameof(message));
|
||||
}
|
||||
|
||||
SmtpSessionState state = new SmtpSessionState {
|
||||
AuthMode = this.Credentials == null ? String.Empty : SmtpDefinitions.SmtpAuthMethods.Login,
|
||||
ClientHostname = ClientHostname,
|
||||
IsChannelSecure = EnableSsl,
|
||||
SenderAddress = message.From.Address,
|
||||
};
|
||||
|
||||
if(this.Credentials != null) {
|
||||
state.Username = this.Credentials.UserName;
|
||||
state.Password = this.Credentials.Password;
|
||||
}
|
||||
|
||||
foreach(MailAddress recipient in message.To) {
|
||||
state.Recipients.Add(recipient.Address);
|
||||
}
|
||||
|
||||
state.DataBuffer.AddRange(message.ToMimeMessage().ToArray());
|
||||
|
||||
return this.SendMailAsync(state, sessionId, 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 this.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 TcpClient tcpClient = new TcpClient();
|
||||
await tcpClient.ConnectAsync(this.Host, this.Port).ConfigureAwait(false);
|
||||
|
||||
using Connection connection = new Connection(tcpClient, Encoding.UTF8, "\r\n", true, 1000);
|
||||
SmtpSender sender = new SmtpSender(sessionId);
|
||||
|
||||
try {
|
||||
// Read the greeting message
|
||||
sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// EHLO 1
|
||||
await this.SendEhlo(sender, connection, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// STARTTLS
|
||||
if(this.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 this.SendEhlo(sender, connection, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// AUTH
|
||||
if(this.Credentials != null) {
|
||||
ConnectionAuth auth = new ConnectionAuth(connection, sender, this.Credentials);
|
||||
await auth.AuthenticateAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach(SmtpSessionState 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(String 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
|
||||
String 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} {this.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) {
|
||||
this._connection = connection;
|
||||
this._sender = sender;
|
||||
this._credentials = credentials;
|
||||
}
|
||||
|
||||
public async Task AuthenticateAsync(CancellationToken ct) {
|
||||
this._sender.RequestText = $"{SmtpCommandNames.AUTH} {SmtpDefinitions.SmtpAuthMethods.Login} {Convert.ToBase64String(Encoding.UTF8.GetBytes(this._credentials.UserName))}";
|
||||
|
||||
await this._connection.WriteLineAsync(this._sender.RequestText, ct).ConfigureAwait(false);
|
||||
this._sender.ReplyText = await this._connection.ReadLineAsync(ct).ConfigureAwait(false);
|
||||
this._sender.ValidateReply();
|
||||
this._sender.RequestText = Convert.ToBase64String(Encoding.UTF8.GetBytes(this._credentials.Password));
|
||||
|
||||
await this._connection.WriteLineAsync(this._sender.RequestText, ct).ConfigureAwait(false);
|
||||
this._sender.ReplyText = await this._connection.ReadLineAsync(ct).ConfigureAwait(false);
|
||||
this._sender.ValidateReply();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
namespace Swan.Net.Smtp
|
||||
{
|
||||
using System;
|
||||
|
||||
namespace Swan.Net.Smtp {
|
||||
/// <summary>
|
||||
/// Contains useful constants and definitions.
|
||||
/// </summary>
|
||||
public static class SmtpDefinitions {
|
||||
/// <summary>
|
||||
/// Contains useful constants and definitions.
|
||||
/// The string sequence that delimits the end of the DATA command.
|
||||
/// </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";
|
||||
}
|
||||
}
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+52
-59
@@ -1,60 +1,53 @@
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
using Swan.Logging;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Mail;
|
||||
|
||||
namespace Swan.Net.Smtp {
|
||||
/// <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) => this._sessionId = sessionId;
|
||||
|
||||
public String RequestText {
|
||||
get => this._requestText;
|
||||
set {
|
||||
this._requestText = value;
|
||||
$" TX {this._requestText}".Trace(typeof(SmtpClient), this._sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
public String ReplyText {
|
||||
get; set;
|
||||
}
|
||||
|
||||
public Boolean IsReplyOk => this.ReplyText.StartsWith("250 ", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public void ValidateReply() {
|
||||
if(this.ReplyText == null) {
|
||||
throw new SmtpException("There was no response from the server");
|
||||
}
|
||||
|
||||
try {
|
||||
SmtpServerReply response = SmtpServerReply.Parse(this.ReplyText);
|
||||
$" RX {this.ReplyText} - {response.IsPositive}".Trace(typeof(SmtpClient), this._sessionId);
|
||||
|
||||
if(response.IsPositive) {
|
||||
return;
|
||||
}
|
||||
|
||||
String 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: {this.ReplyText}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+253
-240
@@ -1,243 +1,256 @@
|
||||
namespace Swan.Net.Smtp
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Swan.Net.Smtp {
|
||||
/// <summary>
|
||||
/// Represents an SMTP server response object.
|
||||
/// </summary>
|
||||
public class SmtpServerReply {
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Represents an SMTP server response object.
|
||||
/// Initializes a new instance of the <see cref="SmtpServerReply"/> class.
|
||||
/// </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
|
||||
}
|
||||
/// <param name="responseCode">The response code.</param>
|
||||
/// <param name="statusCode">The status code.</param>
|
||||
/// <param name="content">The content.</param>
|
||||
public SmtpServerReply(Int32 responseCode, String statusCode, params String[] content) {
|
||||
this.Content = new List<String>();
|
||||
this.ReplyCode = responseCode;
|
||||
this.EnhancedStatusCode = statusCode;
|
||||
this.Content.AddRange(content);
|
||||
this.IsValid = responseCode >= 200 && responseCode < 600;
|
||||
this.ReplyCodeSeverity = SmtpReplyCodeSeverities.Unknown;
|
||||
this.ReplyCodeCategory = SmtpReplyCodeCategories.Unknown;
|
||||
|
||||
if(!this.IsValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(responseCode >= 200) {
|
||||
this.ReplyCodeSeverity = SmtpReplyCodeSeverities.PositiveCompletion;
|
||||
}
|
||||
|
||||
if(responseCode >= 300) {
|
||||
this.ReplyCodeSeverity = SmtpReplyCodeSeverities.PositiveIntermediate;
|
||||
}
|
||||
|
||||
if(responseCode >= 400) {
|
||||
this.ReplyCodeSeverity = SmtpReplyCodeSeverities.TransientNegative;
|
||||
}
|
||||
|
||||
if(responseCode >= 500) {
|
||||
this.ReplyCodeSeverity = SmtpReplyCodeSeverities.PermanentNegative;
|
||||
}
|
||||
|
||||
if(responseCode >= 600) {
|
||||
this.ReplyCodeSeverity = SmtpReplyCodeSeverities.Unknown;
|
||||
}
|
||||
|
||||
if(Int32.TryParse(responseCode.ToString(CultureInfo.InvariantCulture).Substring(1, 1), out Int32 middleDigit)) {
|
||||
if(middleDigit >= 0 && middleDigit <= 5) {
|
||||
this.ReplyCodeCategory = (SmtpReplyCodeCategories)middleDigit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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(Int32 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(Int32 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 Int32 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 Boolean IsValid {
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is positive.
|
||||
/// </summary>
|
||||
public Boolean IsPositive => this.ReplyCode >= 200 && this.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) {
|
||||
String[] lines = text.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if(lines.Length == 0) {
|
||||
return new SmtpServerReply();
|
||||
}
|
||||
|
||||
String[] lastLineParts = lines.Last().Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries);
|
||||
String enhancedStatusCode = String.Empty;
|
||||
_ = Int32.TryParse(lastLineParts[0], out Int32 responseCode);
|
||||
if(lastLineParts.Length > 1) {
|
||||
if(lastLineParts[1].Split('.').Length == 3) {
|
||||
enhancedStatusCode = lastLineParts[1];
|
||||
}
|
||||
}
|
||||
|
||||
List<String> content = new List<String>();
|
||||
|
||||
for(Int32 i = 0; i < lines.Length; i++) {
|
||||
String splitChar = i == lines.Length - 1 ? " " : "-";
|
||||
|
||||
String[] lineParts = lines[i].Split(new[] { splitChar }, 2, StringSplitOptions.None);
|
||||
String lineContent = lineParts.Last();
|
||||
if(String.IsNullOrWhiteSpace(enhancedStatusCode) == false) {
|
||||
lineContent = lineContent.Replace(enhancedStatusCode, String.Empty).Trim();
|
||||
}
|
||||
|
||||
content.Add(lineContent);
|
||||
}
|
||||
|
||||
return new SmtpServerReply(responseCode, enhancedStatusCode, content.ToArray());
|
||||
}
|
||||
|
||||
/// <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() {
|
||||
String responseCodeText = this.ReplyCode.ToString(CultureInfo.InvariantCulture);
|
||||
String statusCodeText = String.IsNullOrWhiteSpace(this.EnhancedStatusCode) ? String.Empty : $" {this.EnhancedStatusCode.Trim()}";
|
||||
if(this.Content.Count == 0) {
|
||||
return $"{responseCodeText}{statusCodeText}";
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
for(Int32 i = 0; i < this.Content.Count; i++) {
|
||||
Boolean isLastLine = i == this.Content.Count - 1;
|
||||
|
||||
_ = builder.Append(isLastLine ? $"{responseCodeText}{statusCodeText} {this.Content[i]}" : $"{responseCodeText}-{this.Content[i]}\r\n");
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+176
-155
@@ -1,158 +1,179 @@
|
||||
namespace Swan.Net.Smtp
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
|
||||
namespace Swan.Net.Smtp {
|
||||
/// <summary>
|
||||
/// Represents the state of an SMTP session associated with a client.
|
||||
/// </summary>
|
||||
public class SmtpSessionState {
|
||||
/// <summary>
|
||||
/// Represents the state of an SMTP session associated with a client.
|
||||
/// Initializes a new instance of the <see cref="SmtpSessionState"/> class.
|
||||
/// </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
|
||||
}
|
||||
public SmtpSessionState() {
|
||||
this.DataBuffer = new List<Byte>();
|
||||
this.Reset(true);
|
||||
this.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 Boolean HasInitiated {
|
||||
get; set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the current session supports extensions.
|
||||
/// </summary>
|
||||
public Boolean 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 Boolean 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 Boolean 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 Boolean HasProvidedUsername => String.IsNullOrWhiteSpace(this.Username) == false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance is authenticated.
|
||||
/// </summary>
|
||||
public Boolean 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 Boolean IsChannelSecure {
|
||||
get; set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the authentication state.
|
||||
/// </summary>
|
||||
public void ResetAuthentication() {
|
||||
this.Username = String.Empty;
|
||||
this.Password = String.Empty;
|
||||
this.AuthMode = String.Empty;
|
||||
this.IsInAuthMode = false;
|
||||
this.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() {
|
||||
this.IsInDataMode = false;
|
||||
this.Recipients.Clear();
|
||||
this.SenderAddress = String.Empty;
|
||||
this.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(Boolean clearExtensionData) {
|
||||
this.HasInitiated = false;
|
||||
this.SupportsExtensions = false;
|
||||
this.ClientHostname = String.Empty;
|
||||
this.ResetEmail();
|
||||
|
||||
if(clearExtensionData) {
|
||||
this.ExtendedData = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new object that is a copy of the current instance.
|
||||
/// </summary>
|
||||
/// <returns>A clone.</returns>
|
||||
public virtual SmtpSessionState Clone() {
|
||||
SmtpSessionState clonedState = this.CopyPropertiesToNew<SmtpSessionState>(new[] { nameof(this.DataBuffer) });
|
||||
clonedState.DataBuffer.AddRange(this.DataBuffer);
|
||||
clonedState.Recipients.AddRange(this.Recipients);
|
||||
|
||||
return clonedState;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user