diff --git a/SMTPServer/SMTPServer.sln b/MailServer/MailServer.sln similarity index 85% rename from SMTPServer/SMTPServer.sln rename to MailServer/MailServer.sln index 0591f69..68d057c 100644 --- a/SMTPServer/SMTPServer.sln +++ b/MailServer/MailServer.sln @@ -1,9 +1,9 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.25914.0 +VisualStudioVersion = 15.0.25920.0 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SMTPServer", "SMTPServer\SMTPServer.csproj", "{ABB6A3E6-38B6-4D02-AC9C-91FA69CF03BE}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MailServer", "MailServer\MailServer.csproj", "{ABB6A3E6-38B6-4D02-AC9C-91FA69CF03BE}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D43013F8-933F-4ADC-8943-08E91662A070}" EndProject diff --git a/MailServer/MailServer/DNS/DnsErrorCode.cs b/MailServer/MailServer/DNS/DnsErrorCode.cs new file mode 100644 index 0000000..dbbd1df --- /dev/null +++ b/MailServer/MailServer/DNS/DnsErrorCode.cs @@ -0,0 +1,199 @@ +////using System; +////using System.Collections.Generic; + +////namespace DnsClient +////{ +//// /* +//// * Reference RFC6895#section-2.3 +//// * +//// RCODE Name Description Reference +//// Decimal +//// Hexadecimal + +//// 0 NoError No Error [RFC1035] +//// 1 FormErr Format Error [RFC1035] +//// 2 ServFail Server Failure [RFC1035] +//// 3 NXDomain Non-Existent Domain [RFC1035] +//// 4 NotImp Not Implemented [RFC1035] +//// 5 Refused Query Refused [RFC1035] +//// 6 YXDomain Name Exists when it should not [RFC2136] +//// 7 YXRRSet RR Set Exists when it should not [RFC2136] +//// 8 NXRRSet RR Set that should exist does not [RFC2136] +//// 9 NotAuth Server Not Authoritative for zone [RFC2136] +//// 9 NotAuth Not Authorized [RFC2845] +//// 10 NotZone Name not contained in zone [RFC2136] + +//// 11 - 15 +//// 0xB - 0xF Unassigned + +//// 16 BADVERS Bad OPT Version [RFC6891] +//// 16 BADSIG TSIG Signature Failure [RFC2845] +//// 17 BADKEY Key not recognized [RFC2845] +//// 18 BADTIME Signature out of time window [RFC2845] +//// 19 BADMODE Bad TKEY Mode [RFC2930] +//// 20 BADNAME Duplicate key name [RFC2930] +//// 21 BADALG Algorithm not supported [RFC2930] +//// 22 BADTRUNC Bad Truncation [RFC4635] + +//// 23 - 3,840 +//// 0x0017 - 0x0F00 Unassigned + +//// 3,841 - 4,095 +//// 0x0F01 - 0x0FFF Reserved for Private Use + +//// 4,096 - 65,534 +//// 0x1000 - 0xFFFE Unassigned + +//// 65,535 +//// 0xFFFF Reserved; can only be allocated by Standards +//// Action. +//// */ + +//// public enum DnsErrorCode : ushort +//// { +//// NoError = 0, +//// FormErr = 1, +//// ServFail = 2, +//// NXDomain = 3, +//// NotImp = 4, +//// Refused = 5, +//// YXDomain = 6, +//// YXRRSet = 7, +//// NXRRSet = 8, +//// NotAuth = 9, +//// NotZone = 10, +//// BADVERS = 16, // or BADSIG +//// BADKEY = 17, +//// BADTIME = 18, +//// BADMODE = 19, +//// BADNAME = 20, +//// BADALG = 21, +//// BADTRUNC = 22, +//// BADCOOKIE = 23, +//// Unassigned = 666 +//// } + +//// public static class DnsErrorCodeText +//// { +//// public const string BADALG = "Algorithm not supported"; +//// public const string BADCOOKIE = "Bad/missing Server Cookie"; +//// public const string BADKEY = "Key not recognized"; +//// public const string BADMODE = "Bad TKEY Mode"; +//// public const string BADNAME = "Duplicate key name"; +//// public const string BADSIG = "TSIG Signature Failure"; +//// public const string BADTIME = "Signature out of time window"; +//// public const string BADTRUNC = "Bad Truncation"; +//// public const string BADVERS = "Bad OPT Version"; +//// public const string FormErr = "Format Error"; +//// public const string NoError = "No Error"; +//// public const string NotAuth = "Server Not Authoritative for zone or Not Authorized"; +//// public const string NotImp = "Not Implemented"; +//// public const string NotZone = "Name not contained in zone"; +//// public const string NXDomain = "Non-Existent Domain"; +//// public const string NXRRSet = "RR Set that should exist does not"; +//// public const string Refused = "Query Refused"; +//// public const string ServFail = "Server Failure"; +//// public const string Unassigned = "Unknown Error"; +//// public const string YXDomain = "Name Exists when it should not"; +//// public const string YXRRSet = "RR Set Exists when it should not"; + +//// private static readonly Dictionary errors = new Dictionary() +//// { +//// { DnsErrorCode.NoError, DnsErrorCodeText.NoError }, +//// { DnsErrorCode.FormErr, DnsErrorCodeText.FormErr }, +//// { DnsErrorCode.ServFail, DnsErrorCodeText.ServFail }, +//// { DnsErrorCode.NXDomain, DnsErrorCodeText.NXDomain }, +//// { DnsErrorCode.NotImp, DnsErrorCodeText.NotImp }, +//// { DnsErrorCode.Refused, DnsErrorCodeText.Refused }, +//// { DnsErrorCode.YXDomain, DnsErrorCodeText.YXDomain }, +//// { DnsErrorCode.YXRRSet, DnsErrorCodeText.YXRRSet }, +//// { DnsErrorCode.NXRRSet, DnsErrorCodeText.NXRRSet }, +//// { DnsErrorCode.NotAuth, DnsErrorCodeText.NotAuth }, +//// { DnsErrorCode.NotZone, DnsErrorCodeText.NotZone }, +//// { DnsErrorCode.BADVERS, DnsErrorCodeText.BADVERS }, +//// { DnsErrorCode.BADKEY, DnsErrorCodeText.BADKEY }, +//// { DnsErrorCode.BADTIME, DnsErrorCodeText.BADTIME }, +//// { DnsErrorCode.BADMODE, DnsErrorCodeText.BADMODE }, +//// { DnsErrorCode.BADNAME, DnsErrorCodeText.BADNAME }, +//// { DnsErrorCode.BADALG, DnsErrorCodeText.BADALG }, +//// { DnsErrorCode.BADTRUNC, DnsErrorCodeText.BADTRUNC }, +//// { DnsErrorCode.BADCOOKIE, DnsErrorCodeText.BADCOOKIE }, +//// }; + +//// public static string GetErrorText(DnsErrorCode code) +//// { +//// if (!errors.ContainsKey(code)) +//// { +//// return Unassigned; +//// } + +//// return errors[code]; +//// } +//// } + +//// public class DnsErrorException : Exception +//// { +//// public DnsErrorCode Code { get; } + +//// public string DnsError { get; } + +//// /// +//// /// Creates an instance of with . +//// /// +//// public DnsErrorException() : base(DnsErrorCodeText.Unassigned) +//// { +//// Code = DnsErrorCode.Unassigned; +//// DnsError = DnsErrorCodeText.GetErrorText(Code); +//// } + +//// /// +//// /// Creates an instance of with +//// /// and a custom message. +//// /// +//// public DnsErrorException(string message) : base(message) +//// { +//// Code = DnsErrorCode.Unassigned; +//// DnsError = DnsErrorCodeText.GetErrorText(Code); +//// } + +//// /// +//// /// Creates an instance of with +//// /// the standard error text for this . +//// /// +//// public DnsErrorException(DnsErrorCode code) : base(DnsErrorCodeText.GetErrorText(code)) +//// { +//// Code = code; +//// DnsError = DnsErrorCodeText.GetErrorText(Code); +//// } + +//// /// +//// /// Creates an instance of with +//// /// and a custom message. +//// /// +//// public DnsErrorException(string message, Exception innerException) : base(message, innerException) +//// { +//// Code = DnsErrorCode.Unassigned; +//// DnsError = DnsErrorCodeText.GetErrorText(Code); +//// } + +//// /// +//// /// Creates an instance of with a custom message +//// /// and the given . +//// /// +//// public DnsErrorException(DnsErrorCode code, string message) : base(message) +//// { +//// Code = code; +//// DnsError = DnsErrorCodeText.GetErrorText(Code); +//// } + +//// /// +//// /// Creates an instance of with a custom message +//// /// and the given . +//// /// +//// public DnsErrorException(DnsErrorCode code, string message, Exception innerException) : base(message, innerException) +//// { +//// Code = code; +//// DnsError = DnsErrorCodeText.GetErrorText(Code); +//// } +//// } +////} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/DnsHeaderFlag.cs b/MailServer/MailServer/DNS/DnsHeaderFlag.cs new file mode 100644 index 0000000..82c0440 --- /dev/null +++ b/MailServer/MailServer/DNS/DnsHeaderFlag.cs @@ -0,0 +1,47 @@ +using System; + +namespace DnsClient +{ + /* Reference: https://tools.ietf.org/html/rfc6895#section-2 + * Response header fields + * + 1 1 1 1 1 1 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + | ID | + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + ==> |QR| OpCode |AA|TC|RD|RA| Z|AD|CD| RCODE | <== + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + | QDCOUNT/ZOCOUNT | + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + | ANCOUNT/PRCOUNT | + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + | NSCOUNT/UPCOUNT | + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + | ARCOUNT | + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + + * */ + + /// + /// The flags of the header's second 16bit value + /// + public enum DnsHeaderFlag : ushort + { + IsCheckingDisabled = 0x0010, + IsAuthenticData = 0x0020, + FutureUse = 0x0040, // Z bit seems to be ignored now, see https://tools.ietf.org/html/rfc6895#section-2.1 + RecursionAvailable = 0x0080, + RecursionDesired = 0x0100, + ResultTruncated = 0x0200, + HasAuthorityAnswer = 0x0400, + HasQuery = 0x8000, + } + + public static class DnsHeader + { + public static readonly ushort OPCODE_MASK = 0x7800; + public static readonly ushort OPCODE_SHIFT = 11; + public static readonly ushort RCODE_MASK = 0x000F; + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/DnsMessageHandler.cs b/MailServer/MailServer/DNS/DnsMessageHandler.cs new file mode 100644 index 0000000..d11e639 --- /dev/null +++ b/MailServer/MailServer/DNS/DnsMessageHandler.cs @@ -0,0 +1,101 @@ +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using DnsClient.Protocol; + +namespace DnsClient +{ + public abstract class DnsMessageHandler : IDisposable + { + private bool _disposedValue = false; + + public abstract Task QueryAsync(IPEndPoint server, DnsRequestMessage request, CancellationToken cancellationToken); + + public abstract bool IsTransientException(T exception) where T : Exception; + + public virtual byte[] GetRequestData(DnsRequestMessage request) + { + var question = request.Question; + var questionData = question.QueryName.AsBytes(); + + // 4 more bytes for the type and class + var writer = new DnsDatagramWriter(DnsRequestHeader.HeaderLength + questionData.Length + 4); + + writer.SetInt16Network((short)request.Header.Id); + writer.SetUInt16Network(request.Header.RawFlags); + writer.SetInt16Network((short)request.Header.QuestionCount); + + // jump to end of header, we didn't write all fields + writer.Index = DnsRequestHeader.HeaderLength; + + writer.SetBytes(questionData, questionData.Length); + writer.SetUInt16Network((ushort)question.QuestionType); + writer.SetUInt16Network((ushort)question.QuestionClass); + + return writer.Data; + } + + public virtual DnsResponseMessage GetResponseMessage(byte[] responseData) + { + var reader = new DnsDatagramReader(responseData); + var factory = new DnsRecordFactory(reader); + + var id = reader.ReadUInt16Reverse(); + var flags = reader.ReadUInt16Reverse(); + var questionCount = reader.ReadUInt16Reverse(); + var answerCount = reader.ReadUInt16Reverse(); + var nameServerCount = reader.ReadUInt16Reverse(); + var additionalCount = reader.ReadUInt16Reverse(); + + var header = new DnsResponseHeader(id, flags, questionCount, answerCount, additionalCount, nameServerCount); + var response = new DnsResponseMessage(header); + + for (int questionIndex = 0; questionIndex < questionCount; questionIndex++) + { + var question = new DnsQuestion(reader.ReadName(), (QueryType)reader.ReadUInt16Reverse(), (QueryClass)reader.ReadUInt16Reverse()); + response.AddQuestion(question); + } + + for (int answerIndex = 0; answerIndex < answerCount; answerIndex++) + { + var info = factory.ReadRecordInfo(); + var record = factory.GetRecord(info); + response.AddAnswer(record); + } + + for (int serverIndex = 0; serverIndex < nameServerCount; serverIndex++) + { + var info = factory.ReadRecordInfo(); + var record = factory.GetRecord(info); + response.AddAuthority(record); + } + + for (int additionalIndex = 0; additionalIndex < additionalCount; additionalIndex++) + { + var info = factory.ReadRecordInfo(); + var record = factory.GetRecord(info); + response.AddAdditional(record); + } + + return response; + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + } + + _disposedValue = true; + } + } + + public void Dispose() + { + Dispose(true); + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/DnsName.cs b/MailServer/MailServer/DNS/DnsName.cs new file mode 100644 index 0000000..af8c3ff --- /dev/null +++ b/MailServer/MailServer/DNS/DnsName.cs @@ -0,0 +1,350 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace DnsClient +{ + public class DnsName : IComparable + { + private const byte ReferenceByte = 0xc0; + private List _labels = new List(); + private short _octets = 1; + + public bool HasRootLabel => (_labels.Count > 0 && Get(0).Equals("")); + + public bool IsEmpty => Size == 0; + + public bool IsHostName => !_labels.Any(p => !IsHostNameLabel(p)); + + public int Octets => _octets; + + public int Size => _labels.Where(p => p != "").Count(); + + /// + /// Creates an empty instance. + /// + public DnsName() + { + Add(0, ""); + } + + /// + /// Initializes a new instance of by parsing the . + /// + /// The input name. + public DnsName(string name) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (name.Length > 0) + { + Parse(name); + } + + if (!HasRootLabel) Add(0, ""); + } + + public static DnsName FromBytes(byte[] data, ref int offset) + { + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + if (offset > data.Length - 1) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + var result = new DnsName(); + + // read the length byte for the label, then get the content from offset+1 to length + // proceed till we reach zero length byte. + byte length; + while ((length = data[offset++]) != 0) + { + // respect the reference bit and lookup the name at the given position + // the reader will advance only for the 2 bytes read. + if ((length & ReferenceByte) != 0) + { + var subset = (length & 0x3f) << 8 | data[offset++]; + var subName = FromBytes(data, ref subset); + result.Concat(subName); + return result; + } + + if (offset + length > data.Length - 1) + { + throw new ArgumentOutOfRangeException( + nameof(data), + $"Found invalid label position {offset - 1} or length {length} in the source data."); + } + + var label = Encoding.ASCII.GetString(data, offset, length); + result.Add(1, label); + offset += length; + } + + return result; + } + + public void Add(int pos, string label) + { + if (label == null) + { + throw new ArgumentNullException(nameof(label)); + } + if (pos < 0 || pos > _labels.Count) + { + throw new ArgumentOutOfRangeException(nameof(pos)); + } + // Check for empty labels: may have only one, and only at end. + int len = label.Length; + if ((pos > 0 && len == 0) || + (pos == 0 && HasRootLabel)) + { + throw new InvalidOperationException("Empty label must be the last label in a domain name"); + } + + // Total length must not be larger than 255 characters (including the ending zero). + if (len > 0) + { + if (_octets + len + 1 >= 256) + { + throw new InvalidOperationException("Name too long"); + } + _octets += (short)(len + 1); + } + + int i = _labels.Count - pos; + VerifyLabel(label); + _labels.Insert(i, label); + } + + public byte[] AsBytes() + { + var bytes = new byte[_octets]; + var offset = 0; + for (int i = _labels.Count - 1; i >= 0; i--) + { + var label = Get(i); + + // should never cause issues as each label's length is limited to 64 chars. + var len = checked((byte)label.Length); + + // set the label length byte + bytes[offset++] = len; + + // set the label's content + var labelBytes = Encoding.ASCII.GetBytes(label); + Array.ConstrainedCopy(labelBytes, 0, bytes, offset, len); + + offset += len; + } + + return bytes; + } + + public int CompareTo(object obj) + { + if (obj == null) + { + return 1; + } + + return ToString().CompareTo(obj.ToString()); + } + + public void Concat(DnsName other) + { + if (other == null) + { + throw new ArgumentNullException(nameof(other)); + } + + foreach (var label in other._labels.Where(p => !string.IsNullOrWhiteSpace(p))) + { + Add(1, label); + } + } + + public override bool Equals(object obj) + { + if (obj == null) + { + return false; + } + + var otherName = obj as DnsName; + if (otherName == null) + { + return false; + } + + return CompareTo(otherName) == 0; + } + + public string Get(int pos) + { + if (pos < 0 || pos > _labels.Count) + { + throw new ArgumentOutOfRangeException(nameof(pos)); + } + + return _labels[_labels.Count - pos - 1]; + } + + public override int GetHashCode() + { + return ToString().GetHashCode(); + } + + public override string ToString() + { + var buf = new StringBuilder(); + foreach (var label in _labels) + { + if (buf.Length > 0 || label.Length == 0) + { + buf.Append('.'); + } + + Escaped(buf, label); + } + + var name = buf.ToString(); + + return name; + } + + private static bool IsHostNameChar(char c) + { + return (c == '-' || + c >= 'a' && c <= 'z' || + c >= 'A' && c <= 'Z' || + c >= '0' && c <= '9'); + } + + private static bool IsHostNameLabel(string label) + { + for (int i = 0; i < label.Length; i++) + { + char c = label.ElementAt(i); + if (!IsHostNameChar(c)) + { + return false; + } + } + return !(label.StartsWith("-") || label.EndsWith("-")); + } + + private static void VerifyLabel(string label) + { + // http://www.freesoft.org/CIE/RFC/1035/9.htm + // dns name limits are 63octets per label + // (63 letters).(63 letters).(63 letters).(62 letters) + if (label.Length > 63) + { + throw new InvalidOperationException("Label exceeds 63 octets: " + label); + } + + // Check for two-byte characters. + for (int i = 0; i < label.Length; i++) + { + char c = label.ElementAt(i); + if ((c & 0xFF00) != 0) + { + throw new InvalidOperationException("Label has two-byte char: " + label); + } + } + } + + private void Escaped(StringBuilder buf, string label) + { + for (int i = 0; i < label.Length; i++) + { + char c = label.ElementAt(i); + if (c == '.' || c == '\\') + { + buf.Append('\\'); + } + + buf.Append(c); + } + } + + private char GetEscaped(string domainName, int pos) + { + try + { + // assert (name.charAt(pos) == '\\'); + char c1 = domainName.ElementAt(++pos); + if (IsDigit(c1)) + { + // sequence is `\DDD' + char c2 = domainName.ElementAt(++pos); + char c3 = domainName.ElementAt(++pos); + if (IsDigit(c2) && IsDigit(c3)) + { + return (char)((c1 - '0') * 100 + (c2 - '0') * 10 + (c3 - '0')); + } + else + { + throw new ArgumentException("Invalid escape sequence.", nameof(domainName)); + } + } + else + { + return c1; + } + } + catch (ArgumentOutOfRangeException) + { + throw new ArgumentException("Invalid escape sequence.", nameof(domainName)); + } + } + + private bool IsDigit(char c) + { + return (c >= '0' && c <= '9'); + } + + private void Parse(string domainName) + { + var label = new StringBuilder(); + + for (int index = 0; index < domainName.Length; index++) + { + var c = domainName[index]; + + if (c == '\\') + { + c = GetEscaped(domainName, index++); + if (IsDigit(domainName[index])) + { + index += 2; + } + + label.Append(c); + } + else if (c != '.') + { + label.Append(c); + } + else + { + Add(0, label.ToString()); + label.Clear(); + } + } + + if (label.Length > 0) + { + Add(0, label.ToString()); + } + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/DnsOpCode.cs b/MailServer/MailServer/DNS/DnsOpCode.cs new file mode 100644 index 0000000..2636b9e --- /dev/null +++ b/MailServer/MailServer/DNS/DnsOpCode.cs @@ -0,0 +1,66 @@ +using System; + +namespace DnsClient +{ + /* + * + * Reference: [RFC6895][RFC1035] + 0 Query [RFC1035] + 1 IQuery (Inverse Query, OBSOLETE) [RFC3425] + 2 Status [RFC1035] + 3 Unassigned + 4 Notify [RFC1996] + 5 Update [RFC2136] + 6-15 Unassigned + * */ + + /// + /// RFCs 1035, 1996, 2136, 3425. + /// Specifies kind of query in this message. + /// This value is set by the originator of a query and copied into the response. + /// + public enum DnsOpCode : short + { + /// + /// RFC 1035. + /// A standard query. + /// + Query, + + /// + /// RFC 3425. + /// An inverse query. + /// + [Obsolete] + IQuery, + + /// + /// RFC 1035. + /// A server status request. + /// + Status, + + Unassinged3, + + /// + /// RFC 1996. + /// + Notify, + + /// + /// RFC 2136. + /// + Update, + + Unassinged6, + Unassinged7, + Unassinged8, + Unassinged9, + Unassinged10, + Unassinged11, + Unassinged12, + Unassinged13, + Unassinged14, + Unassinged15, + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/DnsQueryResponse.cs b/MailServer/MailServer/DNS/DnsQueryResponse.cs new file mode 100644 index 0000000..9b19c23 --- /dev/null +++ b/MailServer/MailServer/DNS/DnsQueryResponse.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using DnsClient.Protocol; +using DnsClient.Protocol.Record; + +namespace DnsClient +{ + /// + /// Immutable version of the . + /// + public class DnsQueryResponse + { + private int? _hashCode; + + /// + /// Gets a list of additional records. + /// + public IReadOnlyCollection Additionals { get; } + + /// + /// Gets a list of all answers, addtional and authority records. + /// + public IReadOnlyCollection AllRecords + { + get + { + return Answers.Concat(Additionals).Concat(Authorities).ToArray(); + } + } + + /// + /// Gets a list of answer records. + /// + public IReadOnlyCollection Answers { get; } + + /// + /// Gets a list of authority records. + /// + public IReadOnlyCollection Authorities { get; } + + /// + /// Returns a string value representing the error response code in case an error occured, otherwise empty. + /// + public string ErrorMessage => HasError ? DnsResponseCodeText.GetErrorText(Header.ResponseCode) : string.Empty; + + /// + /// A flag indicating if the header contains a response codde other than . + /// + public bool HasError => Header?.ResponseCode != DnsResponseCode.NoError; + + /// + /// Gets the header of the response. + /// + public DnsResponseHeader Header { get; } + + /// + /// Gets the list of questions. + /// + public IReadOnlyCollection Questions { get; } + + /// + /// Creates a new instace of . + /// + /// + public DnsQueryResponse( + DnsResponseHeader header, + IReadOnlyCollection questions, + IReadOnlyCollection answers, + IReadOnlyCollection additionals, + IReadOnlyCollection authorities) + { + if (header == null) throw new ArgumentNullException(nameof(header)); + if (questions == null) throw new ArgumentNullException(nameof(questions)); + if (answers == null) throw new ArgumentNullException(nameof(answers)); + if (additionals == null) throw new ArgumentNullException(nameof(additionals)); + if (authorities == null) throw new ArgumentNullException(nameof(authorities)); + + Header = header; + Questions = questions; + Answers = answers; + Additionals = additionals; + Authorities = authorities; + } + + /// + public override bool Equals(object obj) + { + if (obj == null) + { + return false; + } + + var response = obj as DnsQueryResponse; + if (response == null) + { + return false; + } + + return + Header.ToString().Equals(response.Header.ToString()) + && string.Join("", Questions).Equals(string.Join("", response.Questions)) + && string.Join("", AllRecords).Equals(string.Join("", response.AllRecords)); + } + + /// + public override int GetHashCode() + { + if (!_hashCode.HasValue) + { + _hashCode = (Header.ToString() + string.Join("", Questions) + string.Join("", AllRecords)).GetHashCode(); + } + + return _hashCode.Value; + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/DnsQuestion.cs b/MailServer/MailServer/DNS/DnsQuestion.cs new file mode 100644 index 0000000..4f42ae0 --- /dev/null +++ b/MailServer/MailServer/DNS/DnsQuestion.cs @@ -0,0 +1,40 @@ +using System; + +namespace DnsClient +{ + public class DnsQuestion + { + public DnsName QueryName { get; } + + public QueryClass QuestionClass { get; } + + public QueryType QuestionType { get; } + + public DnsQuestion(string queryName, QueryType questionType, QueryClass questionClass) + : this(new DnsName(queryName), questionType, questionClass) + { + } + + public DnsQuestion(DnsName queryName, QueryType questionType, QueryClass questionClass) + { + if (queryName == null) + { + throw new ArgumentNullException(nameof(queryName)); + } + + QueryName = queryName; + QuestionType = questionType; + QuestionClass = questionClass; + } + + public override string ToString() + { + return ToString(0); + } + + public string ToString(int offset = -32) + { + return string.Format("{0,"+offset+"} \t{1} \t{2}", QueryName, QuestionClass, QuestionType); + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/DnsRequestHeader.cs b/MailServer/MailServer/DNS/DnsRequestHeader.cs new file mode 100644 index 0000000..18eab72 --- /dev/null +++ b/MailServer/MailServer/DNS/DnsRequestHeader.cs @@ -0,0 +1,75 @@ +namespace DnsClient +{ + public class DnsRequestHeader + { + public const int HeaderLength = 12; + + private ushort _flags = 0; + + public ushort RawFlags => _flags; + + public DnsHeaderFlag HeaderFlags + { + get + { + return (DnsHeaderFlag)_flags; + } + set + { + _flags &= (ushort)~(DnsHeaderFlag.IsCheckingDisabled); + _flags &= (ushort)~(DnsHeaderFlag.IsAuthenticData); + _flags &= (ushort)~(DnsHeaderFlag.FutureUse); + _flags &= (ushort)~(DnsHeaderFlag.HasQuery); + _flags &= (ushort)~(DnsHeaderFlag.HasAuthorityAnswer); + _flags &= (ushort)~(DnsHeaderFlag.ResultTruncated); + _flags &= (ushort)~(DnsHeaderFlag.RecursionDesired); + _flags &= (ushort)~(DnsHeaderFlag.RecursionAvailable); + _flags |= (ushort)value; + } + } + + public int Id { get; set; } + + public DnsOpCode OpCode + { + get + { + return (DnsOpCode)((DnsHeader.OPCODE_MASK & _flags) >> DnsHeader.OPCODE_SHIFT); + } + set + { + _flags &= (ushort)~(DnsHeader.OPCODE_MASK); + _flags |= (ushort)(((ushort)value << DnsHeader.OPCODE_SHIFT) & DnsHeader.OPCODE_MASK); + } + } + + public int QuestionCount { get; set; } + + public bool UseRecursion + { + get { return (HeaderFlags | DnsHeaderFlag.RecursionDesired) != 0; } + set + { + HeaderFlags |= DnsHeaderFlag.RecursionDesired; + } + } + + public DnsRequestHeader(int id, int questionCount, DnsOpCode queryKind) + : this(id, questionCount, true, queryKind) + { + } + + public DnsRequestHeader(int id, int questionCount, bool useRecursion, DnsOpCode queryKind) + { + Id = id; + QuestionCount = questionCount; + OpCode = queryKind; + UseRecursion = useRecursion; + } + + public override string ToString() + { + return $"{Id} - Qs: {QuestionCount} Recursion: {UseRecursion} OpCode: {OpCode}"; + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/DnsResponseCode.cs b/MailServer/MailServer/DNS/DnsResponseCode.cs new file mode 100644 index 0000000..a9d8d77 --- /dev/null +++ b/MailServer/MailServer/DNS/DnsResponseCode.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections.Generic; + +namespace DnsClient +{ + /* + * Reference RFC6895#section-2.3 + */ + // + /// RFCs 1035, 2136, 2671, 2845, 2930, 4635. + /// + public enum DnsResponseCode : ushort + { + /// + /// RFC 1035. + /// No error condition + /// + NoError = 0, + + /// + /// RFC 1035. + /// Format error. The name server was unable to interpret the query. + /// + FormatError = 1, + + /// + /// RFC 1035. + /// Server failure. The name server was unable to process this query due to a problem with the name server. + /// + ServerFailure = 2, + + /// + /// RFC 1035. + /// Name Error. Meaningful only for responses from an authoritative name server, + /// this code signifies that the domain name referenced in the query does not exist. + /// + NotExistentDomain = 3, + + /// + /// RFC 1035. + /// Not Implemented. The name server does not support the requested kind of query. + /// + NotImplemented = 4, + + /// + /// RFC 1035. + /// Refused. The name server refuses to perform the specified operation for policy reasons. + /// For example, a name server may not wish to provide the information to the particular requester, + /// or a name server may not wish to perform a particular operation (e.g., zone transfer) for particular data. + /// + Refused = 5, + + /// + /// RFC 2136. + /// Name Exists when it should not. + /// + ExistingDomain = 6, + + /// + /// RFC 2136. + /// Resource record set exists when it should not. + /// + ExistingResourceRecordSet = 7, + + /// + /// RFC 2136. + /// Resource record set that should exist but does not. + /// + MissingResourceRecordSet = 8, + + /// + /// RFC 2136 / RFC2845 + /// Server Not Authoritative for zone / Not Authorized. + /// + NotAuthorized = 9, + + /// + /// RFC 2136. + /// Name not contained in zone. + /// + NotZone = 10, + + /// + /// RFCs 2671 / 2845. + /// Bad OPT Version or TSIG Signature Failure. + /// + BadVersionOrBadSignature = 16, + + /// + /// RFC 2845. + /// Key not recognized. + /// + BadKey = 17, + + /// + /// RFC 2845. + /// Signature out of time window. + /// + BadTime = 18, + + /// + /// RFC 2930. + /// Bad TKEY Mode. + /// + BadMode = 19, + + /// + /// RFC 2930. + /// Duplicate key name. + /// + BadName = 20, + + /// + /// RFC 2930. + /// Algorithm not supported. + /// + BadAlgorithm = 21, + + /// + /// RFC 4635. + /// BADTRUNC - Bad Truncation. + /// + BadTruncation = 22, + + /// + /// RFC 7873 + /// Bad/missing Server Cookie + /// + BadCookie = 23, + + /// + /// Unknown error. + /// + Unassigned = 666 + } + + + public static class DnsResponseCodeText + { + internal const string BADALG = "Algorithm not supported"; + internal const string BADCOOKIE = "Bad/missing Server Cookie"; + internal const string BADKEY = "Key not recognized"; + internal const string BADMODE = "Bad TKEY Mode"; + internal const string BADNAME = "Duplicate key name"; + internal const string BADSIG = "TSIG Signature Failure"; + internal const string BADTIME = "Signature out of time window"; + internal const string BADTRUNC = "Bad Truncation"; + internal const string BADVERS = "Bad OPT Version"; + internal const string FormErr = "Format Error"; + internal const string NoError = "No Error"; + internal const string NotAuth = "Server Not Authoritative for zone or Not Authorized"; + internal const string NotImp = "Not Implemented"; + internal const string NotZone = "Name not contained in zone"; + internal const string NXDomain = "Non-Existent Domain"; + internal const string NXRRSet = "RR Set that should exist does not"; + internal const string Refused = "Query Refused"; + internal const string ServFail = "Server Failure"; + internal const string Unassigned = "Unknown Error"; + internal const string YXDomain = "Name Exists when it should not"; + internal const string YXRRSet = "RR Set Exists when it should not"; + + private static readonly Dictionary errors = new Dictionary() + { + { DnsResponseCode.NoError, DnsResponseCodeText.NoError }, + { DnsResponseCode.FormatError, DnsResponseCodeText.FormErr }, + { DnsResponseCode.ServerFailure, DnsResponseCodeText.ServFail }, + { DnsResponseCode.NotExistentDomain, DnsResponseCodeText.NXDomain }, + { DnsResponseCode.NotImplemented, DnsResponseCodeText.NotImp }, + { DnsResponseCode.Refused, DnsResponseCodeText.Refused }, + { DnsResponseCode.ExistingDomain, DnsResponseCodeText.YXDomain }, + { DnsResponseCode.ExistingResourceRecordSet, DnsResponseCodeText.YXRRSet }, + { DnsResponseCode.MissingResourceRecordSet, DnsResponseCodeText.NXRRSet }, + { DnsResponseCode.NotAuthorized, DnsResponseCodeText.NotAuth }, + { DnsResponseCode.NotZone, DnsResponseCodeText.NotZone }, + { DnsResponseCode.BadVersionOrBadSignature, DnsResponseCodeText.BADVERS }, + { DnsResponseCode.BadKey, DnsResponseCodeText.BADKEY }, + { DnsResponseCode.BadTime, DnsResponseCodeText.BADTIME }, + { DnsResponseCode.BadMode, DnsResponseCodeText.BADMODE }, + { DnsResponseCode.BadName, DnsResponseCodeText.BADNAME }, + { DnsResponseCode.BadAlgorithm, DnsResponseCodeText.BADALG }, + { DnsResponseCode.BadTruncation, DnsResponseCodeText.BADTRUNC }, + { DnsResponseCode.BadCookie, DnsResponseCodeText.BADCOOKIE }, + }; + + public static string GetErrorText(DnsResponseCode code) + { + if (!errors.ContainsKey(code)) + { + return Unassigned; + } + + return errors[code]; + } + } + + public class DnsResponseException : Exception + { + public DnsResponseCode Code { get; } + + public string DnsError { get; } + + /// + /// Creates an instance of with . + /// + public DnsResponseException() : base(DnsResponseCodeText.Unassigned) + { + Code = DnsResponseCode.Unassigned; + DnsError = DnsResponseCodeText.GetErrorText(Code); + } + + /// + /// Creates an instance of with + /// and a custom message. + /// + public DnsResponseException(string message) : base(message) + { + Code = DnsResponseCode.Unassigned; + DnsError = DnsResponseCodeText.GetErrorText(Code); + } + + /// + /// Creates an instance of with + /// the standard error text for this . + /// + public DnsResponseException(DnsResponseCode code) : base(DnsResponseCodeText.GetErrorText(code)) + { + Code = code; + DnsError = DnsResponseCodeText.GetErrorText(Code); + } + + /// + /// Creates an instance of with + /// and a custom message. + /// + public DnsResponseException(string message, Exception innerException) : base(message, innerException) + { + Code = DnsResponseCode.Unassigned; + DnsError = DnsResponseCodeText.GetErrorText(Code); + } + + /// + /// Creates an instance of with a custom message + /// and the given . + /// + public DnsResponseException(DnsResponseCode code, string message) : base(message) + { + Code = code; + DnsError = DnsResponseCodeText.GetErrorText(Code); + } + + /// + /// Creates an instance of with a custom message + /// and the given . + /// + public DnsResponseException(DnsResponseCode code, string message, Exception innerException) : base(message, innerException) + { + Code = code; + DnsError = DnsResponseCodeText.GetErrorText(Code); + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/DnsResponseHeader.cs b/MailServer/MailServer/DNS/DnsResponseHeader.cs new file mode 100644 index 0000000..e2f16b0 --- /dev/null +++ b/MailServer/MailServer/DNS/DnsResponseHeader.cs @@ -0,0 +1,57 @@ +namespace DnsClient +{ + public class DnsResponseHeader + { + private readonly ushort _flags = 0; + + public int AdditionalCount { get; } + + public int AnswerCount { get; } + + public bool FutureUse => HasFlag(DnsHeaderFlag.FutureUse); + + public bool HasAuthorityAnswer => HasFlag(DnsHeaderFlag.HasAuthorityAnswer); + + public DnsHeaderFlag HeaderFlags => (DnsHeaderFlag)_flags; + + public int Id { get; } + + public bool IsAuthenticData => HasFlag(DnsHeaderFlag.IsAuthenticData); + + public bool IsCheckingDisabled => HasFlag(DnsHeaderFlag.IsCheckingDisabled); + + public bool HasQuery => HasFlag(DnsHeaderFlag.HasQuery); + + public int NameServerCount { get; } + + public DnsOpCode OPCode => (DnsOpCode)((DnsHeader.OPCODE_MASK & _flags) >> DnsHeader.OPCODE_SHIFT); + + public int QuestionCount { get; } + + public bool RecursionAvailable => HasFlag(DnsHeaderFlag.RecursionAvailable); + + public DnsResponseCode ResponseCode => (DnsResponseCode)(_flags & DnsHeader.RCODE_MASK); + + ////ResponseCode {set + ////{ + //// _flags &= (ushort)~(DnsHeader.RCODE_MASK); + //// _flags |= (ushort)((ushort)value & DnsHeader.RCODE_MASK); + ////}} + + public bool ResultTruncated => HasFlag(DnsHeaderFlag.ResultTruncated); + + public bool RecursionDesired => HasFlag(DnsHeaderFlag.RecursionDesired); + + public DnsResponseHeader(int id, ushort flags, int questionCount, int answerCount, int additionalCount, int serverCount) + { + Id = id; + _flags = flags; + QuestionCount = questionCount; + AnswerCount = answerCount; + AdditionalCount = additionalCount; + NameServerCount = serverCount; + } + + private bool HasFlag(DnsHeaderFlag flag) => (HeaderFlags & flag) != 0; + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/DnsUdpMessageHandler.cs b/MailServer/MailServer/DNS/DnsUdpMessageHandler.cs new file mode 100644 index 0000000..9f7a787 --- /dev/null +++ b/MailServer/MailServer/DNS/DnsUdpMessageHandler.cs @@ -0,0 +1,64 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using DnsClient.Protocol; + +namespace DnsClient +{ + public class DnsUdpMessageHandler : DnsMessageHandler, IDisposable + { + private readonly UdpClient _client = new UdpClient(); + private bool _disposedValue = false; + + public override bool IsTransientException(T exception) + { + Debug.WriteLine("Check transient {0}.", exception); + if (exception is SocketException) return true; + return false; + } + + public override async Task QueryAsync( + IPEndPoint server, + DnsRequestMessage request, + CancellationToken cancellationToken) + { + var sw = Stopwatch.StartNew(); + + //using (var udpClient = new UdpClient()) + //{ + var data = GetRequestData(request); + await _client.SendAsync(data, data.Length, server); + + var result = await _client.ReceiveAsync(); + + var response = GetResponseMessage(result.Buffer); + + if (request.Header.Id != response.Header.Id) + { + throw new DnsResponseException("Header id missmatch."); + } + + return response; + //} + } + + protected override void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _client.Dispose(); + } + + _disposedValue = true; + } + + base.Dispose(disposing); + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/LookupClient.cs b/MailServer/MailServer/DNS/LookupClient.cs new file mode 100644 index 0000000..185b775 --- /dev/null +++ b/MailServer/MailServer/DNS/LookupClient.cs @@ -0,0 +1,368 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using DnsClient.Protocol; + +namespace DnsClient +{ + public class LookupClient : IDisposable + { + private static readonly TimeSpan s_defaultTimeout = TimeSpan.FromSeconds(5); + private static readonly TimeSpan s_infiniteTimeout = System.Threading.Timeout.InfiniteTimeSpan; + private static readonly TimeSpan s_maxTimeout = TimeSpan.FromMilliseconds(int.MaxValue); + private static ushort _uniqueId = 0; + private readonly ResponseCache _cache = new ResponseCache(true); + private readonly object _endpointLock = new object(); + private readonly DnsMessageHandler _messageHandler; + private Queue _endpoints; + private TimeSpan _timeout = s_defaultTimeout; + private bool _disposedValue = false; + + /// + /// Gets the list of configured name servers. + /// + public IReadOnlyCollection NameServers { get; } + + /// + /// Gets or set a flag indicating if recursion should be enabled for DNS queries. + /// + public bool Recursion { get; set; } = true; + + /// + /// Gets or sets number of tries to connect to one name server before trying the next one or throwing an exception. + /// + public int Retries { get; set; } = 5; + + /// + /// Gets or sets a flag indicating if the should throw an + /// if the returned result contains an error flag other than . + /// (The default behavior is False). + /// + public bool ThrowDnsErrors { get; set; } = false; + + /// + /// Gets or sets timeout in milliseconds. + /// Timeout must be greater than zero and less than . + /// + public TimeSpan Timeout + { + get { return _timeout; } + set + { + if ((value <= TimeSpan.Zero || value > s_maxTimeout) && value != s_infiniteTimeout) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + _timeout = value; + } + } + + /// + /// Gets or sets a flag indicating if the should use caching or not. + /// The TTL of cached results is defined by each resource record individually. + /// + public bool UseCache + { + get + { + return _cache.Enabled; + } + set + { + _cache.Enabled = value; + } + } + + /// + /// Gets or sets a which can override the TTL of a resource record in case the + /// TTL of the record is lower than this minimum value. + /// This is useful in cases where the server retruns a zero TTL and the record should be cached for a + /// very short duration anyways. + /// + /// This setting gets igonred in case is set to False. + /// + public TimeSpan? MimimumCacheTimeout + { + get + { + return _cache.MinimumTimout; + } + set + { + _cache.MinimumTimout = value; + } + } + + public LookupClient() + : this(NameServer.ResolveNameServers().ToArray()) + { + } + + public LookupClient(params IPEndPoint[] nameServers) + : this(new DnsUdpMessageHandler(), nameServers) + { + } + + public LookupClient(params IPAddress[] nameServers) + : this( + new DnsUdpMessageHandler(), + nameServers.Select(p => new IPEndPoint(p, NameServer.DefaultPort)).ToArray()) + { + } + + public LookupClient(DnsMessageHandler messageHandler, ICollection nameServers) + { + if (messageHandler == null) + { + throw new ArgumentNullException(nameof(messageHandler)); + } + if (nameServers == null || nameServers.Count == 0) + { + throw new ArgumentException("At least one name server must be configured.", nameof(nameServers)); + } + + NameServers = nameServers.ToArray(); + _endpoints = new Queue(); + foreach (var server in NameServers) + { + _endpoints.Enqueue(new EndPointInfo(server)); + } + _messageHandler = messageHandler; + } + + /// + /// Translates the IPV4 or IPV6 address into an arpa address. + /// + /// IP address to get the arpa address form + /// The mirrored IPV4 or IPV6 arpa address + public static string GetArpaName(IPAddress ip) + { + var bytes = ip.GetAddressBytes(); + Array.Reverse(bytes); + + // check IP6 + if (ip.AddressFamily == AddressFamily.InterNetworkV6) + { + // reveresed bytes need to be split into 4 bit parts and separated by '.' + var newBytes = bytes + .SelectMany(b => new[] { (b >> 0) & 0xf, (b >> 4) & 0xf }) + .Aggregate(new StringBuilder(), (s, b) => s.Append(b.ToString("x")).Append(".")) + "ip6.arpa."; + + return newBytes; + } + else if (ip.AddressFamily == AddressFamily.InterNetwork) + { + // else IP4 + return string.Join(".", bytes) + ".in-addr.arpa."; + } + + throw new InvalidOperationException("Not a valid IP4 or IP6 address."); + } + + public Task QueryAsync(string query, QueryType queryType) + => QueryAsync(query, queryType, CancellationToken.None); + + public Task QueryAsync(string query, QueryType queryType, CancellationToken cancellationToken) + => QueryAsync(query, queryType, QueryClass.IN, cancellationToken); + + public Task QueryAsync(string query, QueryType queryType, QueryClass queryClass) + => QueryAsync(query, queryType, queryClass, CancellationToken.None); + + public Task QueryAsync(string query, QueryType queryType, QueryClass queryClass, CancellationToken cancellationToken) + => QueryAsync(new DnsQuestion(query, queryType, queryClass), cancellationToken); + + ////public Task QueryAsync(params DnsQuestion[] questions) + //// => QueryAsync(CancellationToken.None, questions); + + private async Task QueryAsync(DnsQuestion question, CancellationToken cancellationToken) + { + if (question == null) + { + throw new ArgumentNullException(nameof(question)); + } + + var head = new DnsRequestHeader(GetNextUniqueId(), 1, Recursion, DnsOpCode.Query); + var request = new DnsRequestMessage(head, question); + var cacheKey = ResponseCache.GetCacheKey(question); + var result = await _cache.GetOrAdd(cacheKey, async () => await ResolveQueryAsync(request, cancellationToken)); + + return result; + } + + public Task QueryReverseAsync(IPAddress ipAddress) + => QueryReverseAsync(ipAddress, CancellationToken.None); + + public Task QueryReverseAsync(IPAddress ipAddress, CancellationToken cancellationToken) + { + if (ipAddress == null) + { + throw new ArgumentNullException(nameof(ipAddress)); + } + + var arpa = GetArpaName(ipAddress); + return QueryAsync(arpa, QueryType.PTR, QueryClass.IN, cancellationToken); + } + + private static ushort GetNextUniqueId() + { + if (_uniqueId == ushort.MaxValue || _uniqueId == 0) + { + _uniqueId = (ushort)(new Random()).Next(ushort.MaxValue / 2); + } + + return _uniqueId++; + } + + // TODO: TCP fallback on truncates + // TODO: most popular DNS servers do not support mulitple queries in one packet, therefore, split it into multiple requests? + //private async Task QueryAsync(DnsRequestMessage request, CancellationToken cancellationToken) + //{ + + //} + + private async Task ResolveQueryAsync(DnsRequestMessage request, CancellationToken cancellationToken) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + for (int index = 0; index < NameServers.Count; index++) + { + EndPointInfo serverInfo = null; + lock (_endpointLock) + { + while (_endpoints.Count > 0 && serverInfo == null) + { + serverInfo = _endpoints.Dequeue(); + + if (serverInfo.IsDisabled) + { + serverInfo = null; + } + else + { + // put it back and then use it.. + _endpoints.Enqueue(serverInfo); + } + } + + if (serverInfo == null) + { + // let's be optimistic and eable them again, maybe they wher offline one for a while + _endpoints.ToList().ForEach(p => p.IsDisabled = false); + + continue; + } + } + + var tries = 0; + do + { + tries++; + try + { + DnsResponseMessage response; + var resultTask = _messageHandler.QueryAsync(serverInfo.Endpoint, request, cancellationToken); + if (Timeout != s_infiniteTimeout) + { + response = await resultTask.TimeoutAfter(Timeout); + } + + response = await resultTask; + + var result = response.AsReadonly; + + if (ThrowDnsErrors && result.Header.ResponseCode != DnsResponseCode.NoError) + { + throw new DnsResponseException(result.Header.ResponseCode); + } + + return result; + } + catch (DnsResponseException) + { + // occurs only if the option to throw dns exceptions is enabled on the lookup client. (see above). + // lets not mess with the stack + throw; + } + catch (TimeoutException) + { + // do nothing... transient if timeoutAfter timed out + } + catch (SocketException ex) when (ex.SocketErrorCode == SocketError.AddressFamilyNotSupported) + { + // this socket error might indicate the server endpoint is actually bad and should be ignored in future queries. + serverInfo.IsDisabled = true; + Debug.WriteLine($"Disabling name server {serverInfo.Endpoint}."); + break; + } + catch (Exception ex) when (_messageHandler.IsTransientException(ex)) + { + } + catch (Exception ex) + { + var agg = ex as AggregateException; + if (agg != null) + { + agg.Handle(e => + { + if (e is TimeoutException) return true; + if (_messageHandler.IsTransientException(e)) return true; + return false; + }); + + throw new DnsResponseException("Unhandled exception", agg.InnerException); + } + + throw new DnsResponseException("Unhandled exception", ex); + } + finally + { + // do cleanup stuff or logging? + } + } while (tries <= Retries && !cancellationToken.IsCancellationRequested); + } + + throw new DnsResponseException($"No connection could be established to any of the following name servers: {string.Join(", ", NameServers)}."); + } + + private class EndPointInfo + { + public IPEndPoint Endpoint { get; } + + public bool IsDisabled { get; set; } + + public EndPointInfo(IPEndPoint endpoint) + { + Endpoint = endpoint; + IsDisabled = false; + } + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _messageHandler.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + Dispose(true); + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/NameServer.cs b/MailServer/MailServer/DNS/NameServer.cs new file mode 100644 index 0000000..0817b5a --- /dev/null +++ b/MailServer/MailServer/DNS/NameServer.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; + +namespace DnsClient +{ + public class NameServer + { + /// + /// The default DNS server port. + /// + public const int DefaultPort = 53; + + /// + /// Gets a list of name servers by iterating over the available network interfaces. + /// + /// The list of name servers. + public static ICollection ResolveNameServers() + { + var result = new HashSet(); + + var adapters = NetworkInterface.GetAllNetworkInterfaces(); + foreach (NetworkInterface networkInterface in + adapters + .Where(p => p.OperationalStatus == OperationalStatus.Up + && p.NetworkInterfaceType != NetworkInterfaceType.Loopback)) + { + foreach (IPAddress dnsAddress in networkInterface + .GetIPProperties() + .DnsAddresses + .Where(i => + i.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork + || i.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6)) + { + result.Add(new IPEndPoint(dnsAddress, DefaultPort)); + } + } + + return result.ToArray(); + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/Protocol/DnsDatagramReader.cs b/MailServer/MailServer/DNS/Protocol/DnsDatagramReader.cs new file mode 100644 index 0000000..d05c024 --- /dev/null +++ b/MailServer/MailServer/DNS/Protocol/DnsDatagramReader.cs @@ -0,0 +1,135 @@ +using System; +using System.Net; +using System.Text; + +namespace DnsClient.Protocol +{ + public class DnsDatagramReader + { + private readonly byte[] _data; + private int _index; + + public int Index + { + get + { + return _index; + } + set + { + if (value < 0 || value > _data.Length) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + _index = value; + } + } + + public DnsDatagramReader(byte[] data, int startIndex = 0) + { + _data = data; + Index = startIndex; + } + + /* + https://tools.ietf.org/html/rfc1035#section-3.3: + is a single + length octet followed by that number of characters. + is treated as binary information, and can be up to 256 characters in + length (including the length octet). + * */ + /// + /// Reads the single length octet and the following characters as ASCII text. + /// + /// + public string ReadString() + { + var length = ReadByte(); + + var result = Encoding.ASCII.GetString(_data, _index, length); + _index += length; + return result; + } + + public byte ReadByte() + { + if (_index >= _data.Length) + { + throw new IndexOutOfRangeException("Cannot read byte."); + } + else + { + return _data[_index++]; + } + } + + public byte[] ReadBytes(int length) + { + if (_data.Length < _index + length) + { + throw new IndexOutOfRangeException($"Cannot read that many bytes: '{length}'."); + } + + var result = new byte[length]; + Array.Copy(_data, _index, result, 0, length); + _index += length; + return result; + } + + /// + /// Reads an IP address from the next 4 bytes. + /// + /// The . + /// If there are no 4 bytes to read. + public IPAddress ReadIPAddress() + { + if (_data.Length < _index + 4) + { + throw new IndexOutOfRangeException("IPAddress expected exactly 4 bytes."); + } + + return new IPAddress(ReadBytes(4)); + } + + public IPAddress ReadIPv6Address() + { + var address = new IPAddress(ReadBytes(8 * 2)); + + return address; + } + + public DnsName ReadName() + { + return DnsName.FromBytes(_data, ref _index); + } + + public ushort ReadUInt16() + { + if (_data.Length < Index + 2) + { + throw new IndexOutOfRangeException("Cannot read more data."); + } + + var result = BitConverter.ToUInt16(_data, _index); + _index += 2; + return result; + } + + public ushort ReadUInt16Reverse() + { + if (_data.Length < Index + 2) + { + throw new IndexOutOfRangeException("Cannot read more data."); + } + + byte a = _data[_index++], b = _data[_index++]; + return (ushort)(a << 8 | b); + } + + public uint ReadUInt32Reverse() + { + return (uint)(ReadUInt16Reverse() << 16 | ReadUInt16Reverse()); + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/Protocol/DnsDatagramWriter.cs b/MailServer/MailServer/DNS/Protocol/DnsDatagramWriter.cs new file mode 100644 index 0000000..8d845da --- /dev/null +++ b/MailServer/MailServer/DNS/Protocol/DnsDatagramWriter.cs @@ -0,0 +1,113 @@ +using System; +using System.Net; + +namespace DnsClient.Protocol +{ + public class DnsDatagramWriter + { + private byte[] _buffer; + + public byte[] Data => _buffer; + + public int Index { get; set; } + + public DnsDatagramWriter(int length) + { + _buffer = new byte[length]; + } + + private DnsDatagramWriter(byte[] data, int newLength) + { + if (data.Length > newLength) + { + throw new ArgumentOutOfRangeException(nameof(newLength)); + } + + _buffer = new byte[newLength]; + Array.Copy(data, _buffer, data.Length); + } + + /// + /// Creates a new writer instance with a new length. + /// + /// The amount of bytes the current buffer should be extended by. + /// A new writer. + public void Extend(int byLength) + { + if (byLength <= 0) + { + throw new ArgumentOutOfRangeException(nameof(byLength)); + } + + var fullLength = byLength + _buffer.Length; + var newBuffer = new byte[fullLength]; + Array.Copy(_buffer, 0, newBuffer, 0, _buffer.Length); + //return new DnsDatagramWriter(newBuffer, fullLength) { Offset = Offset }; + _buffer = newBuffer; + } + + public void SetBytes(byte[] data, int length) => SetBytes(data, 0, Index, length); + + public void SetInt(int value) => SetInt(value, Index); + + public void SetInt16(short value) => SetInt16(value, Index); + + public void SetInt16Network(short value) => SetInt16Network(value, Index); + + public void SetUInt32Network(uint value) + { + var bytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((int)value)); + SetBytes(bytes, Index, bytes.Length); + } + + public void SetIntNetwork(int value) => SetIntNetwork(value, Index); + + public void SetUInt16(ushort value) => SetInt16((short)value, Index); + + public void SetUInt16Network(ushort value) => SetInt16Network((short)value, Index); + + private void SetBytes(byte[] data, int destOffset, int length) + { + SetBytes(data, 0, destOffset, length); + } + + private void SetBytes(byte[] data, int dataOffset, int destOffset, int length) + { + if (length + dataOffset > data.Length || length + dataOffset > _buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + if (destOffset + dataOffset + length > _buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(destOffset)); + } + + Array.ConstrainedCopy(data, dataOffset, _buffer, destOffset, length); + Index = destOffset + length; + } + + private void SetInt(int value, int offset) + { + var bytes = BitConverter.GetBytes(value); + SetBytes(bytes, offset, bytes.Length); + } + + private void SetInt16(short value, int offset) + { + var bytes = BitConverter.GetBytes(value); + SetBytes(bytes, offset, bytes.Length); + } + + private void SetInt16Network(short value, int offset) + { + var bytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(value)); + SetBytes(bytes, offset, bytes.Length); + } + + private void SetIntNetwork(int value, int offset) + { + var bytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(value)); + SetBytes(bytes, offset, bytes.Length); + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/Protocol/DnsRecordFactory.cs b/MailServer/MailServer/DNS/Protocol/DnsRecordFactory.cs new file mode 100644 index 0000000..edfb1f5 --- /dev/null +++ b/MailServer/MailServer/DNS/Protocol/DnsRecordFactory.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections.Generic; +using DnsClient.Protocol.Record; + +namespace DnsClient.Protocol +{ + public class DnsRecordFactory + { + public static IDictionary> s_recordFactory = + new Dictionary>(); + + private readonly DnsDatagramReader _reader; + + public DnsRecordFactory(DnsDatagramReader reader) + { + if (reader == null) + { + throw new ArgumentNullException(nameof(reader)); + } + + _reader = reader; + } + + /* + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + | | + / / + / NAME / + | | + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + | TYPE | + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + | CLASS | + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + | TTL | + | | + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + | RDLENGTH | + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--| + / RDATA / + / / + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + * */ + public ResourceRecordInfo ReadRecordInfo() + { + return new ResourceRecordInfo( + _reader.ReadName().ToString(), // name + (ResourceRecordType)_reader.ReadUInt16Reverse(),// type + (QueryClass)_reader.ReadUInt16Reverse(), // class + _reader.ReadUInt32Reverse(), // ttl - 32bit!! + _reader.ReadUInt16Reverse()); // RDLength + //reader.ReadBytes(reader.ReadUInt16Reverse())); // rdata + } + + public DnsResourceRecord GetRecord(ResourceRecordInfo info) + { + if (info == null) + { + throw new ArgumentNullException(nameof(info)); + } + + var oldIndex = _reader.Index; + DnsResourceRecord result; + + if (s_recordFactory.ContainsKey(info.RecordType)) + { + result = s_recordFactory[info.RecordType](_reader, info); + } + else + { + switch (info.RecordType) + { + case ResourceRecordType.A: + result = ResolveARecord(info); + break; + + case ResourceRecordType.NS: + result = ResolveNsRecord(info); + break; + + case ResourceRecordType.SOA: + result = ResolveSoaRecord(info); + break; + + case ResourceRecordType.PTR: + result = ResolvePtrRecord(info); + break; + + case ResourceRecordType.MX: + result = ResolveMXRecord(info); + break; + + case ResourceRecordType.TXT: + result = ResolveTXTRecord(info); + break; + + case ResourceRecordType.AAAA: + result = ResolveAAAARecord(info); + break; + + case ResourceRecordType.SRV: + result = ResolveSrvRecord(info); + break; + + default: + // update reader index because we don't read full data for the empty record + _reader.Index += info.RawDataLength; + result = new EmptyRecord(info); + break; + } + } + + // sanity check + if (_reader.Index != oldIndex + info.RawDataLength) + { + throw new InvalidOperationException("Record reader index out of sync."); + } + + return result; + } + + private PtrRecord ResolvePtrRecord(ResourceRecordInfo info) + { + return new PtrRecord(info, _reader.ReadName().ToString()); + } + + private AAAARecord ResolveAAAARecord(ResourceRecordInfo info) + { + var address = _reader.ReadIPv6Address(); + return new AAAARecord(info, address); + } + + // default resolver implementation for an A Record + private ARecord ResolveARecord(ResourceRecordInfo info) + { + if (info.RawDataLength != 4) + { + throw new IndexOutOfRangeException($"Reading wrong length for an IP address. Expected 4 found {info.RawDataLength}."); + } + + return new ARecord(info, _reader.ReadIPAddress()); + } + + private MxRecord ResolveMXRecord(ResourceRecordInfo info) + { + var preference = _reader.ReadUInt16Reverse(); + var domain = _reader.ReadName(); + + return new MxRecord(info, preference, domain.ToString()); + } + + private NsRecord ResolveNsRecord(ResourceRecordInfo info) + { + var name = _reader.ReadName(); + return new NsRecord(info, name.ToString()); + } + + private SoaRecord ResolveSoaRecord(ResourceRecordInfo info) + { + var mName = _reader.ReadName(); + var rName = _reader.ReadName(); + var serial = _reader.ReadUInt32Reverse(); + var refresh = _reader.ReadUInt32Reverse(); + var retry = _reader.ReadUInt32Reverse(); + var expire = _reader.ReadUInt32Reverse(); + var minimum = _reader.ReadUInt32Reverse(); + + return new SoaRecord(info, mName.ToString(), rName.ToString(), serial, refresh, retry, expire, minimum); + } + + private SrvRecord ResolveSrvRecord(ResourceRecordInfo info) + { + var priority = _reader.ReadUInt16Reverse(); + var weight = _reader.ReadUInt16Reverse(); + var port = _reader.ReadUInt16Reverse(); + var target = _reader.ReadName(); + + return new SrvRecord(info, priority, weight, port, target.ToString()); + } + + private TxtRecord ResolveTXTRecord(ResourceRecordInfo info) + { + int pos = _reader.Index; + + var values = new List(); + while ((_reader.Index - pos) < info.RawDataLength) + { + values.Add(_reader.ReadString()); + } + + return new TxtRecord(info, values.ToArray()); + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/Protocol/DnsRequestMessage.cs b/MailServer/MailServer/DNS/Protocol/DnsRequestMessage.cs new file mode 100644 index 0000000..213988e --- /dev/null +++ b/MailServer/MailServer/DNS/Protocol/DnsRequestMessage.cs @@ -0,0 +1,34 @@ +using System; +using System.Linq; + +namespace DnsClient.Protocol +{ + /// + /// Represents a simple request message which can be send through . + /// + public class DnsRequestMessage + { + public DnsRequestHeader Header { get; } + + public DnsQuestion Question { get; } + + public DnsRequestMessage(DnsRequestHeader header, DnsQuestion question) + { + if (header == null) + { + throw new ArgumentNullException(nameof(header)); + } + if (question == null ) + { + throw new ArgumentNullException(nameof(question)); + } + if (header.QuestionCount != 1) + { + throw new InvalidOperationException("Header question count and number of questions do not match."); + } + + Header = header; + Question = question; + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/Protocol/DnsResourceRecord.cs b/MailServer/MailServer/DNS/Protocol/DnsResourceRecord.cs new file mode 100644 index 0000000..8d8d361 --- /dev/null +++ b/MailServer/MailServer/DNS/Protocol/DnsResourceRecord.cs @@ -0,0 +1,84 @@ +using System; + +namespace DnsClient.Protocol +{ + public abstract class DnsResourceRecord : ResourceRecordInfo + { + public DnsResourceRecord(ResourceRecordInfo info) + : base(info.QueryName, info.RecordType, info.RecordClass, info.TimeToLive, info.RawDataLength) + { + } + + /// + public override string ToString() + { + return ToString(0); + } + + /// + /// Same as ToString but offsets the + /// by . + /// Set the offset to -32 for example to make it print nicely in consols. + /// + /// The offset. + /// A string representing this instance. + public virtual string ToString(int offset = 0) + { + return string.Format("{0," + offset + "}{1} \t{2} \t{3} \t{4}", + QueryName, + TimeToLive, + RecordClass, + RecordType, + RecordToString()); + } + + /// + /// Returns the actual record's value only and not the full object representation. + /// uses this to compose the full string value of this instance. + /// + /// A string representing this record. + public abstract string RecordToString(); + } + + public class ResourceRecordInfo + { + /// + /// The query name. + /// + public string QueryName { get; } + + /// + /// Specifies type of resource record. + /// + public ResourceRecordType RecordType { get; } + + /// + /// Specifies type class of resource record, mostly IN but can be CS, CH or HS . + /// + public QueryClass RecordClass { get; } + + /// + /// The TTL value for the record set by the server. + /// + public uint TimeToLive { get; } + + /// + /// Gets the number of bytes for this resource record stored in RDATA + /// + public int RawDataLength { get; } + + public ResourceRecordInfo(string queryName, ResourceRecordType recordType, QueryClass recordClass, uint ttl, int length) + { + if (queryName == null) + { + throw new ArgumentNullException(nameof(queryName)); + } + + QueryName = queryName; + RecordType = recordType; + RecordClass = recordClass; + TimeToLive = ttl; + RawDataLength = length; + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/Protocol/DnsResponseMessage.cs b/MailServer/MailServer/DNS/Protocol/DnsResponseMessage.cs new file mode 100644 index 0000000..11f27c7 --- /dev/null +++ b/MailServer/MailServer/DNS/Protocol/DnsResponseMessage.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace DnsClient.Protocol +{ + /// + /// A simple response message which gets returned by the . + /// + public class DnsResponseMessage + { + private readonly IList _additionals = new List(); + private readonly IList _answers = new List(); + private readonly IList _authorities = new List(); + private readonly DnsResponseHeader _header; + private readonly IList _questions = new List(); + + /// + /// Gets the readonly representation of this message which can be returned. + /// + public DnsQueryResponse AsReadonly + => new DnsQueryResponse(_header, _questions.ToArray(), _answers.ToArray(), _additionals.ToArray(), _authorities.ToArray()); + + public DnsResponseHeader Header => _header; + + public DnsResponseMessage(DnsResponseHeader header) + { + if (header == null) + { + throw new ArgumentNullException(nameof(header)); + } + + _header = header; + } + + public void AddAdditional(DnsResourceRecord record) + { + if (record == null) + { + throw new ArgumentNullException(nameof(record)); + } + + _additionals.Add(record); + } + + public void AddAnswer(DnsResourceRecord record) + { + if (record == null) + { + throw new ArgumentNullException(nameof(record)); + } + + _answers.Add(record); + } + + public void AddAuthority(DnsResourceRecord record) + { + if (record == null) + { + throw new ArgumentNullException(nameof(record)); + } + + _authorities.Add(record); + } + + public void AddQuestion(DnsQuestion question) + { + if (question == null) + { + throw new ArgumentNullException(nameof(question)); + } + + _questions.Add(question); + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/Protocol/Record/ARecord.cs b/MailServer/MailServer/DNS/Protocol/Record/ARecord.cs new file mode 100644 index 0000000..23e9c33 --- /dev/null +++ b/MailServer/MailServer/DNS/Protocol/Record/ARecord.cs @@ -0,0 +1,45 @@ +using System; +using System.Net; + +namespace DnsClient.Protocol.Record +{ + /* + 3.4.1. A RDATA format + + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + | ADDRESS | + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + + where: + + ADDRESS A 32 bit Internet address. + + Hosts that have multiple Internet addresses will have multiple A + records. + * + */ + + /// + /// A DNS resource record represending an IP address. + /// Hosts that have multiple Internet addresses will have multiple A records. + /// + public class ARecord : DnsResourceRecord + { + public IPAddress Address { get; } + + public ARecord(ResourceRecordInfo info, IPAddress address) : base(info) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + Address = address; + } + + public override string RecordToString() + { + return Address.ToString(); + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/Protocol/Record/AaaaRecord.cs b/MailServer/MailServer/DNS/Protocol/Record/AaaaRecord.cs new file mode 100644 index 0000000..b92dbb2 --- /dev/null +++ b/MailServer/MailServer/DNS/Protocol/Record/AaaaRecord.cs @@ -0,0 +1,35 @@ +using System; +using System.Net; + +namespace DnsClient.Protocol.Record +{ + /* https://tools.ietf.org/html/rfc3596#section-2.2 + 2.2 AAAA data format + + A 128 bit IPv6 address is encoded in the data portion of an AAAA + resource record in network byte order (high-order byte first). + */ + /// + /// A 128 bit IPv6 address is encoded in the data portion of an AAAA + /// resource record in network byte order(high-order byte first). + /// + public class AAAARecord : DnsResourceRecord + { + public IPAddress Address { get; } + + public AAAARecord(ResourceRecordInfo info, IPAddress address) : base(info) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + Address = address; + } + + public override string RecordToString() + { + return Address.ToString(); + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/Protocol/Record/CaaRecord.cs b/MailServer/MailServer/DNS/Protocol/Record/CaaRecord.cs new file mode 100644 index 0000000..4f52d0b --- /dev/null +++ b/MailServer/MailServer/DNS/Protocol/Record/CaaRecord.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace DnsClient.Protocol.Record +{ + /* RFC 6844 (https://tools.ietf.org/html/rfc6844#section-5.1) + A CAA RR contains a single property entry consisting of a tag-value + pair. Each tag represents a property of the CAA record. The value + of a CAA property is that specified in the corresponding value field. + + A domain name MAY have multiple CAA RRs associated with it and a + given property MAY be specified more than once. + + The CAA data field contains one property entry. A property entry + consists of the following data fields: + + +0-1-2-3-4-5-6-7-|0-1-2-3-4-5-6-7-| + | Flags | Tag Length = n | + +----------------+----------------+...+---------------+ + | Tag char 0 | Tag char 1 |...| Tag char n-1 | + +----------------+----------------+...+---------------+ + +----------------+----------------+.....+----------------+ + | Value byte 0 | Value byte 1 |.....| Value byte m-1 | + +----------------+----------------+.....+----------------+ + + Where n is the length specified in the Tag length field and m is the + remaining octets in the Value field (m = d - n - 2) where d is the + length of the RDATA section. + + The data fields are defined as follows: + + Flags: One octet containing the following fields: + + Bit 0, Issuer Critical Flag: If the value is set to '1', the + critical flag is asserted and the property MUST be understood + if the CAA record is to be correctly processed by a certificate + issuer. + + A Certification Authority MUST NOT issue certificates for any + Domain that contains a CAA critical property for an unknown or + unsupported property tag that for which the issuer critical + flag is set. + + Note that according to the conventions set out in [RFC1035], bit 0 + is the Most Significant Bit and bit 7 is the Least Significant + Bit. Thus, the Flags value 1 means that bit 7 is set while a value + of 128 means that bit 0 is set according to this convention. + + All other bit positions are reserved for future use. + + To ensure compatibility with future extensions to CAA, DNS records + compliant with this version of the CAA specification MUST clear + (set to "0") all reserved flags bits. Applications that interpret + CAA records MUST ignore the value of all reserved flag bits. + + Tag Length: A single octet containing an unsigned integer specifying + the tag length in octets. The tag length MUST be at least 1 and + SHOULD be no more than 15. + + Tag: The property identifier, a sequence of US-ASCII characters. + + Tag values MAY contain US-ASCII characters 'a' through 'z', 'A' + through 'Z', and the numbers 0 through 9. Tag values SHOULD NOT + contain any other characters. Matching of tag values is case + insensitive. + + Tag values submitted for registration by IANA MUST NOT contain any + characters other than the (lowercase) US-ASCII characters 'a' + through 'z' and the numbers 0 through 9. + + Value: A sequence of octets representing the property value. + Property values are encoded as binary values and MAY employ sub- + formats. + + The length of the value field is specified implicitly as the + remaining length of the enclosing Resource Record data field. + * */ + /// + /// Record type 257 + /// The Certification Authority Authorization (CAA) DNS Resource Record + /// allows a DNS domain name holder to specify one or more Certification + /// Authorities(CAs) authorized to issue certificates for that domain. + /// CAA Resource Records allow a public Certification Authority to + /// implement additional controls to reduce the risk of unintended + /// certificate mis-issue.This document defines the syntax of the CAA + /// record and rules for processing CAA records by certificate issuers. + /// + public class CaaRecord + { + } +} diff --git a/MailServer/MailServer/DNS/Protocol/Record/EmptyRecord.cs b/MailServer/MailServer/DNS/Protocol/Record/EmptyRecord.cs new file mode 100644 index 0000000..6999b2c --- /dev/null +++ b/MailServer/MailServer/DNS/Protocol/Record/EmptyRecord.cs @@ -0,0 +1,14 @@ +namespace DnsClient.Protocol.Record +{ + public class EmptyRecord : DnsResourceRecord + { + public EmptyRecord(ResourceRecordInfo info) : base(info) + { + } + + public override string RecordToString() + { + return string.Empty; + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/Protocol/Record/MXRecord.cs b/MailServer/MailServer/DNS/Protocol/Record/MXRecord.cs new file mode 100644 index 0000000..490c105 --- /dev/null +++ b/MailServer/MailServer/DNS/Protocol/Record/MXRecord.cs @@ -0,0 +1,65 @@ +using System; + +namespace DnsClient.Protocol.Record +{ + /* + 3.3.9. MX RDATA format + + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + | PREFERENCE | + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + / EXCHANGE / + / / + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + + where: + + PREFERENCE A 16 bit integer which specifies the preference given to + this RR among others at the same owner. Lower values + are preferred. + + EXCHANGE A which specifies a host willing to act as + a mail exchange for the owner name. + + MX records cause type A additional section processing for the host + specified by EXCHANGE. The use of MX RRs is explained in detail in + [RFC-974]. + */ + + /// + /// MX records cause type A additional section processing for the host + /// specified by EXCHANGE.The use of MX RRs is explained in detail in + /// [RFC-974]. + /// + public class MxRecord : DnsResourceRecord + { + /// + /// Gets a 16 bit integer which specifies the preference given to + /// this RR among others at the same owner. + /// Lower values are preferred. + /// + public ushort Preference { get; } + + /// + /// A which specifies a host willing to act as a mail exchange. + /// + public string Exchange { get; } + + public MxRecord(ResourceRecordInfo info, ushort preference, string domainName) + : base(info) + { + if (string.IsNullOrWhiteSpace(domainName)) + { + throw new ArgumentNullException(nameof(domainName)); + } + + Preference = preference; + Exchange = domainName; + } + + public override string RecordToString() + { + return string.Format("{0} {1}", Preference, Exchange); + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/Protocol/Record/NsRecord.cs b/MailServer/MailServer/DNS/Protocol/Record/NsRecord.cs new file mode 100644 index 0000000..9d5216f --- /dev/null +++ b/MailServer/MailServer/DNS/Protocol/Record/NsRecord.cs @@ -0,0 +1,51 @@ +using System; + +namespace DnsClient.Protocol.Record +{ + /* + https://tools.ietf.org/html/rfc1035#section-3.3.11: + NS RDATA format + + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + / NSDNAME / + / / + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + + where: + + NSDNAME A which specifies a host which should be + authoritative for the specified class and domain. + + NS records cause both the usual additional section processing to locate + a type A record, and, when used in a referral, a special search of the + zone in which they reside for glue information. + + The NS RR states that the named host should be expected to have a zone + starting at owner name of the specified class. Note that the class may + not indicate the protocol family which should be used to communicate + with the host, although it is typically a strong hint. For example, + hosts which are name servers for either Internet (IN) or Hesiod (HS) + class information are normally queried using IN class protocols. + */ + + public class NsRecord : DnsResourceRecord + { + public string NSDName { get; } + + internal NsRecord(ResourceRecordInfo info, string name) + : base(info) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + NSDName = name; + } + + public override string RecordToString() + { + return NSDName; + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/Protocol/Record/PtrRecord.cs b/MailServer/MailServer/DNS/Protocol/Record/PtrRecord.cs new file mode 100644 index 0000000..0fec60d --- /dev/null +++ b/MailServer/MailServer/DNS/Protocol/Record/PtrRecord.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; + +namespace DnsClient.Protocol.Record +{ + /* RFC 1035 (https://tools.ietf.org/html/rfc1035#section-3.3.12) + 3.3.12. PTR RDATA format + + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + / PTRDNAME / + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + + where: + + PTRDNAME A which points to some location in the + domain name space. + + PTR records cause no additional section processing. These RRs are used + in special domains to point to some other location in the domain space. + These records are simple data, and don't imply any special processing + similar to that performed by CNAME, which identifies aliases. See the + description of the IN-ADDR.ARPA domain for an example. + */ + public class PtrRecord : DnsResourceRecord + { + public string PtrDomainName { get; } + + internal PtrRecord(ResourceRecordInfo info, string ptrDName) + : base(info) + { + if (string.IsNullOrWhiteSpace(ptrDName)) + { + throw new ArgumentNullException(nameof(ptrDName)); + } + + PtrDomainName = ptrDName; + } + + public override string RecordToString() + { + return PtrDomainName; + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/Protocol/Record/ResourceRecordCollectionExtensions.cs b/MailServer/MailServer/DNS/Protocol/Record/ResourceRecordCollectionExtensions.cs new file mode 100644 index 0000000..ce416c8 --- /dev/null +++ b/MailServer/MailServer/DNS/Protocol/Record/ResourceRecordCollectionExtensions.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using DnsClient.Protocol; +using DnsClient.Protocol.Record; + +namespace DnsClient +{ + public static class RecordCollectionExtension + { + public static IEnumerable AaaaRecords(this IEnumerable records) + { + return records.OfType(); + } + + public static IEnumerable ARecords(this IEnumerable records) + { + return records.OfType(); + } + + public static IEnumerable CaaRecords(this IEnumerable records) + { + return records.OfType(); + } + + public static IEnumerable NsRecords(this IEnumerable records) + { + return records.OfType(); + } + + public static IEnumerable OfRecordType(this IEnumerable records, ResourceRecordType type) + { + return records.Where(p => p.RecordType == type); + } + + public static IEnumerable PtrRecords(this IEnumerable records) + { + return records.OfType(); + } + + public static IEnumerable SoaRecords(this IEnumerable records) + { + return records.OfType(); + } + + public static IEnumerable SrvRecords(this IEnumerable records) + { + return records.OfType(); + } + + public static IEnumerable TxtRecords(this IEnumerable records) + { + return records.OfType(); + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/Protocol/Record/SoaRecord.cs b/MailServer/MailServer/DNS/Protocol/Record/SoaRecord.cs new file mode 100644 index 0000000..6f6be36 --- /dev/null +++ b/MailServer/MailServer/DNS/Protocol/Record/SoaRecord.cs @@ -0,0 +1,111 @@ +namespace DnsClient.Protocol.Record +{ + /* + 3.3.13. SOA RDATA format + + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + / MNAME / + / / + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + / RNAME / + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + | SERIAL | + | | + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + | REFRESH | + | | + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + | RETRY | + | | + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + | EXPIRE | + | | + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + | MINIMUM | + | | + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + + where: + + MNAME The of the name server that was the + original or primary source of data for this zone. + + RNAME A which specifies the mailbox of the + person responsible for this zone. + + SERIAL The unsigned 32 bit version number of the original copy + of the zone. Zone transfers preserve this value. This + value wraps and should be compared using sequence space + arithmetic. + + REFRESH A 32 bit time interval before the zone should be + refreshed. + + RETRY A 32 bit time interval that should elapse before a + failed refresh should be retried. + + EXPIRE A 32 bit time value that specifies the upper limit on + the time interval that can elapse before the zone is no + longer authoritative. + + MINIMUM The unsigned 32 bit minimum TTL field that should be + exported with any RR from this zone. + + SOA records cause no additional section processing. + + All times are in units of seconds. + + Most of these fields are pertinent only for name server maintenance + operations. However, MINIMUM is used in all query operations that + retrieve RRs from a zone. Whenever a RR is sent in a response to a + query, the TTL field is set to the maximum of the TTL field from the RR + and the MINIMUM field in the appropriate SOA. Thus MINIMUM is a lower + bound on the TTL field for all RRs in a zone. Note that this use of + MINIMUM should occur when the RRs are copied into the response and not + when the zone is loaded from a master file or via a zone transfer. The + reason for this provison is to allow future dynamic update facilities to + change the SOA RR with known semantics. + */ + + public class SoaRecord : DnsResourceRecord + { + public uint Expire { get; } + + public uint Minimum { get; } + + public string MName { get; } + + public uint Refresh { get; } + + public uint Retry { get; } + + public string RName { get; } + + public uint Serial { get; } + + public SoaRecord(ResourceRecordInfo info, string mName, string rName, uint serial, uint refresh, uint retry, uint expire, uint minimum) + : base(info) + { + MName = mName; + RName = rName; + Serial = serial; + Refresh = refresh; + Retry = retry; + Expire = expire; + Minimum = minimum; + } + + public override string RecordToString() + { + return string.Format( + "{0} {1} {2} {3} {4} {5} {6}", + MName, + RName, + Serial, + Refresh, + Retry, + Expire, + Minimum); + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/Protocol/Record/SrvRecord.cs b/MailServer/MailServer/DNS/Protocol/Record/SrvRecord.cs new file mode 100644 index 0000000..71092b9 --- /dev/null +++ b/MailServer/MailServer/DNS/Protocol/Record/SrvRecord.cs @@ -0,0 +1,147 @@ +namespace DnsClient.Protocol.Record +{ + /* RFC 2782 (https://tools.ietf.org/html/rfc2782) + + The format of the SRV RR + + Here is the format of the SRV RR, whose DNS type code is 33: + + _Service._Proto.Name TTL Class SRV Priority Weight Port Target + + (There is an example near the end of this document.) + + Service + The symbolic name of the desired service, as defined in Assigned + Numbers [STD 2] or locally. An underscore (_) is prepended to + the service identifier to avoid collisions with DNS labels that + occur in nature. + Some widely used services, notably POP, don't have a single + universal name. If Assigned Numbers names the service + indicated, that name is the only name which is legal for SRV + lookups. The Service is case insensitive. + + Proto + The symbolic name of the desired protocol, with an underscore + (_) prepended to prevent collisions with DNS labels that occur + in nature. _TCP and _UDP are at present the most useful values + for this field, though any name defined by Assigned Numbers or + locally may be used (as for Service). The Proto is case + insensitive. + + Name + The domain this RR refers to. The SRV RR is unique in that the + name one searches for is not this name; the example near the end + shows this clearly. + + TTL + Standard DNS meaning [RFC 1035]. + + Class + Standard DNS meaning [RFC 1035]. SRV records occur in the IN + Class. + + Priority + The priority of this target host. A client MUST attempt to + contact the target host with the lowest-numbered priority it can + reach; target hosts with the same priority SHOULD be tried in an + order defined by the weight field. The range is 0-65535. This + is a 16 bit unsigned integer in network byte order. + + Weight + A server selection mechanism. The weight field specifies a + relative weight for entries with the same priority. Larger + weights SHOULD be given a proportionately higher probability of + being selected. The range of this number is 0-65535. This is a + 16 bit unsigned integer in network byte order. Domain + administrators SHOULD use Weight 0 when there isn't any server + selection to do, to make the RR easier to read for humans (less + noisy). In the presence of records containing weights greater + than 0, records with weight 0 should have a very small chance of + being selected. + + In the absence of a protocol whose specification calls for the + use of other weighting information, a client arranges the SRV + RRs of the same Priority in the order in which target hosts, + + specified by the SRV RRs, will be contacted. The following + algorithm SHOULD be used to order the SRV RRs of the same + priority: + + To select a target to be contacted next, arrange all SRV RRs + (that have not been ordered yet) in any order, except that all + those with weight 0 are placed at the beginning of the list. + + Compute the sum of the weights of those RRs, and with each RR + associate the running sum in the selected order. Then choose a + uniform random number between 0 and the sum computed + (inclusive), and select the RR whose running sum value is the + first in the selected order which is greater than or equal to + the random number selected. The target host specified in the + selected SRV RR is the next one to be contacted by the client. + Remove this SRV RR from the set of the unordered SRV RRs and + apply the described algorithm to the unordered SRV RRs to select + the next target host. Continue the ordering process until there + are no unordered SRV RRs. This process is repeated for each + Priority. + + Port + The port on this target host of this service. The range is 0- + 65535. This is a 16 bit unsigned integer in network byte order. + This is often as specified in Assigned Numbers but need not be. + + Target + The domain name of the target host. There MUST be one or more + address records for this name, the name MUST NOT be an alias (in + the sense of RFC 1034 or RFC 2181). Implementors are urged, but + not required, to return the address record(s) in the Additional + Data section. Unless and until permitted by future standards + action, name compression is not to be used for this field. + + A Target of "." means that the service is decidedly not + available at this domain. + + */ + + /// + /// The SRV RR allows administrators to use several servers for a single + /// domain, to move services from host to host with little fuss, and to + /// designate some hosts as primary servers for a service and others as + /// backups. + /// + /// Clients ask for a specific service/protocol for a specific domain + /// (the word domain is used here in the strict RFC 1034 sense), and get + /// back the names of any available servers. + /// + /// Note that where this document refers to "address records", it means A + /// RR's, AAAA RR's, or their most modern equivalent. + /// + public class SrvRecord : DnsResourceRecord + { + public ushort Port { get; } + + public ushort Priority { get; } + + public string Target { get; } + + public ushort Weight { get; } + + public SrvRecord(ResourceRecordInfo info, ushort priority, ushort weigth, ushort port, string target) + : base(info) + { + Priority = priority; + Weight = weigth; + Port = port; + Target = target; + } + + public override string RecordToString() + { + return string.Format( + "{0} {1} {2} {3}", + Priority, + Weight, + Port, + Target); + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/Protocol/Record/TxtRecord.cs b/MailServer/MailServer/DNS/Protocol/Record/TxtRecord.cs new file mode 100644 index 0000000..cbba441 --- /dev/null +++ b/MailServer/MailServer/DNS/Protocol/Record/TxtRecord.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; + +namespace DnsClient.Protocol.Record +{ + /* + * RFC 1464 https://tools.ietf.org/html/rfc1464 + + https://tools.ietf.org/html/rfc1035#section-3.3: + is a single + length octet followed by that number of characters. + is treated as binary information, and can be up to 256 characters in + length (including the length octet). + + https://tools.ietf.org/html/rfc1035#section-3.3.14: + TXT RDATA format + + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + / TXT-DATA / + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + + where: + + TXT-DATA One or more s. + + TXT RRs are used to hold descriptive text. The semantics of the text + depends on the domain where it is found. + */ + + /// + /// TXT RRs are used to hold descriptive text. The semantics of the text + /// depends on the domain where it is found. + /// + public class TxtRecord : DnsResourceRecord + { + /// + /// The list of TXT values of this TXT RR. + /// + public IReadOnlyCollection Text { get; } + + public TxtRecord(ResourceRecordInfo info, string[] values) + : base(info) + { + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + Text = values; + } + + public override string RecordToString() + { + return string.Join(" ", Text).Trim(); + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/QueryClass.cs b/MailServer/MailServer/DNS/QueryClass.cs new file mode 100644 index 0000000..4b06152 --- /dev/null +++ b/MailServer/MailServer/DNS/QueryClass.cs @@ -0,0 +1,47 @@ +using System; +using System.Linq; +namespace DnsClient +{ + /* RFC 1035 (https://tools.ietf.org/html/rfc1035#section-3.2.4) + * 3.2.4. CLASS values + * + CLASS fields appear in resource records. The following CLASS mnemonics + and values are defined: + + IN 1 the Internet + + CS 2 the CSNET class (Obsolete - used only for examples in + some obsolete RFCs) + + CH 3 the CHAOS class + + HS 4 Hesiod [Dyer 87] + + */ + + /// + /// CLASS fields appear in resource records. + /// + public enum QueryClass : short + { + /// + /// The Internet. + /// + IN = 1, + + /// + /// The CSNET class (Obsolete - used only for examples in some obsolete RFCs). + /// + CS = 2, + + /// + /// The CHAOS class. + /// + CH = 3, + + /// + /// Hesiod [Dyer 87]. + /// + HS = 4 + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/QueryType.cs b/MailServer/MailServer/DNS/QueryType.cs new file mode 100644 index 0000000..4a78ae4 --- /dev/null +++ b/MailServer/MailServer/DNS/QueryType.cs @@ -0,0 +1,116 @@ +using System; +using System.Linq; +namespace DnsClient +{ + /* + * RFC 1035 (https://tools.ietf.org/html/rfc1035#section-3.2.3) + * */ + + /// + /// QTYPE fields appear in the question part of a query. QTYPES are a superset of TYPEs, hence all TYPEs are valid QTYPEs. + /// + public enum QueryType : short + { + /// + /// A host address [RFC1035]. + /// + A = 1, + + /// + /// An authoritative name server [RFC1035]. + /// + NS = 2, + + /// + /// A mail destination (OBSOLETE - use MX) [RFC1035]. + /// + MD = 3, + + /// + /// A mail forwarder (OBSOLETE - use MX) [RFC1035]. + /// + MF = 4, + + /// + /// The canonical name for an alias [RFC1035]. + /// + CNAME = 5, + + /// + /// Marks the start of a zone of authority [RFC1035]. + /// + SOA = 6, + + /// + /// A mailbox domain name (EXPERIMENTAL) [RFC1035]. TODO:impl + /// + MB = 7, + + /// + /// A mail group member (EXPERIMENTAL) [RFC1035]. TODO:impl + /// + MG = 8, + + /// + /// A mail rename domain name (EXPERIMENTAL) [RFC1035]. TODO:impl + /// + MR = 9, + + /// + /// A null RR (EXPERIMENTAL) [RFC1035]. TODO:impl + /// + NULL = 10, + + /// + /// A well known service description [RFC1035] TODO:impl + /// + WKS = 11, + + /// + /// A domain name pointer [RFC1035] + /// + PTR = 12, + + /// + /// Host information [RFC1035] TODO:impl + /// + HINFO = 13, + + /// + /// Mailbox or mail list information [RFC1035] TODO:impl + /// + MINFO = 14, + + /// + /// Mail exchange [RFC1035] + /// + MX = 15, + + /// + /// Text strings [RFC1035] + /// + TXT = 16, + + /// + /// A IPV6 host address, [RFC3596] + /// + AAAA = 28, + + /// + /// Location of services [RFC2782] + /// + SRV = 33, + + /// + /// RRSIG rfc3755. TODO:impl + /// + RRSIG = 46, + + /// + /// Generic any query *. + /// + ANY = 255, + + CAA = 257, + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/ResourceRecordType.cs b/MailServer/MailServer/DNS/ResourceRecordType.cs new file mode 100644 index 0000000..66cb9d1 --- /dev/null +++ b/MailServer/MailServer/DNS/ResourceRecordType.cs @@ -0,0 +1,114 @@ +using System; +using System.Linq; +namespace DnsClient +{ + /* + * RFC 1035 (https://tools.ietf.org/html/rfc1035#section-3.2.2) + * */ + + /// + /// TYPE fields are used in resource records. Note that these types are a subset of QTYPEs. + /// + public enum ResourceRecordType : short + { + /// + /// A host address [RFC1035]. + /// + A = 1, + + /// + /// An authoritative name server [RFC1035]. + /// + NS = 2, + + /// + /// A mail destination (OBSOLETE - use MX) [RFC1035]. + /// + MD = 3, + + /// + /// A mail forwarder (OBSOLETE - use MX) [RFC1035]. + /// + MF = 4, + + /// + /// The canonical name for an alias [RFC1035]. + /// + CNAME = 5, + + /// + /// Marks the start of a zone of authority [RFC1035]. + /// + SOA = 6, + + /// + /// A mailbox domain name (EXPERIMENTAL) [RFC1035]. + /// + MB = 7, + + /// + /// A mail group member (EXPERIMENTAL) [RFC1035]. + /// + MG = 8, + + /// + /// A mail rename domain name (EXPERIMENTAL) [RFC1035]. + /// + MR = 9, + + /// + /// A null RR (EXPERIMENTAL) [RFC1035]. + /// + NULL = 10, + + /// + /// A well known service description [RFC1035] + /// + WKS = 11, + + /// + /// A domain name pointer [RFC1035] + /// + PTR = 12, + + /// + /// Host information [RFC1035] + /// + HINFO = 13, + + /// + /// Mailbox or mail list information [RFC1035] + /// + MINFO = 14, + + /// + /// Mail exchange [RFC1035] + /// + MX = 15, + + /// + /// Text strings [RFC1035] + /// + TXT = 16, + + /// + /// A IPV6 host address, [RFC3596] + /// + AAAA = 28, + + /// + /// Location of services [RFC2782] + /// + SRV = 33, + + /// + /// RRSIG rfc3755. TODO:impl + /// + RRSIG = 46, + + /// + /// TODO:impl + /// + CAA = 257, + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/ResponseCache.cs b/MailServer/MailServer/DNS/ResponseCache.cs new file mode 100644 index 0000000..ea702a0 --- /dev/null +++ b/MailServer/MailServer/DNS/ResponseCache.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace DnsClient +{ + internal class ResponseCache + { + private static readonly long CleanupInterval = (long)TimeSpan.FromMinutes(10).TotalMilliseconds; + private ConcurrentDictionary _cache = new ConcurrentDictionary(); + private object _cleanupLock = new object(); + private bool _cleanupRunning = false; + private int _lastCleanup = 0; + + public int Count => _cache.Count; + + public bool Enabled { get; set; } + + public TimeSpan? MinimumTimout { get; set; } + + public ResponseCache(bool enabled = true, TimeSpan? minimumTimout = null) + { + Enabled = enabled; + MinimumTimout = minimumTimout; + } + + public static string GetCacheKey(DnsQuestion question) + { + if (question == null) + { + throw new ArgumentNullException(nameof(question)); + } + + return question.QueryName + ":" + question.QuestionClass + ":" + question.QuestionType; + } + + public async Task GetOrAdd(string key, Func> create) + { + if (create == null) + { + throw new ArgumentNullException(nameof(create)); + } + + if (!Enabled) + { + return await create(); + } + + ResponseEntry entry; + if (_cache.TryGetValue(key, out entry)) + { + if (entry.IsExpired) + { + _cache.TryRemove(key, out entry); + } + else + { + return entry.Response; + } + } + + var record = await create(); + + // only cache in case the result is valid and does need caching + if (record != null) + { + var newEntry = CreatedEntry(record); + + // respecting minimum expiration value which gets evaluated in CreateEntry + if (newEntry.TTL > 0) + { + _cache.TryAdd(key, newEntry); + + StartCleanup(); + } + } + + return record; + } + + private static void DoCleanup(ResponseCache cache) + { + cache._cleanupRunning = true; + + foreach (var entry in cache._cache) + { + if (entry.Value.IsExpired) + { + ResponseEntry o; + cache._cache.TryRemove(entry.Key, out o); + } + } + + cache._cleanupRunning = false; + } + + private ResponseEntry CreatedEntry(DnsQueryResponse response) + { + var entry = new ResponseEntry(response); + + // minimum timeout + if (MinimumTimout.HasValue && entry.TTL < MinimumTimout.Value.TotalMilliseconds) + { + entry.TTL = (long)MinimumTimout.Value.TotalMilliseconds; + } + + return entry; + } + + private void StartCleanup() + { + var currentTicks = Environment.TickCount; + if (!_cleanupRunning && _lastCleanup + CleanupInterval < currentTicks) + { + lock (_cleanupLock) + { + if (!_cleanupRunning && _lastCleanup + CleanupInterval < currentTicks) + { + _lastCleanup = currentTicks; + + Task.Factory.StartNew( + state => DoCleanup((ResponseCache)state), + this, + CancellationToken.None, + TaskCreationOptions.DenyChildAttach, + TaskScheduler.Default); + } + } + } + } + + private class ResponseEntry + { + public bool IsExpired => Environment.TickCount > TicksCreated + TTL; + + public DnsQueryResponse Response { get; set; } + + public int TicksCreated { get; } + + public long TTL { get; set; } + + public ResponseEntry(DnsQueryResponse response) + { + var minTtl = response.AllRecords.Min(p => p?.TimeToLive); + + Response = response; + TTL = response.HasError || !minTtl.HasValue ? 0 : (int)minTtl.Value * 1000; // ttl is in second, we calculate in millis + TicksCreated = Environment.TickCount; + } + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/TaskExtensions.cs b/MailServer/MailServer/DNS/TaskExtensions.cs new file mode 100644 index 0000000..d7a0712 --- /dev/null +++ b/MailServer/MailServer/DNS/TaskExtensions.cs @@ -0,0 +1,35 @@ +namespace System.Threading.Tasks +{ + internal static class TaskExtensions + { + public static async Task TimeoutAfter(this Task task, TimeSpan timeout) + { + var cts = new CancellationTokenSource(); + + if (task == await Task.WhenAny(task, Task.Delay((int)timeout.TotalMilliseconds, cts.Token))) + { + cts.Cancel(); + await task; + } + else + { + throw new TimeoutException(); + } + } + + public static async Task TimeoutAfter(this Task task, TimeSpan timeout) + { + var cts = new CancellationTokenSource(); + + if (task == await Task.WhenAny(task, Task.Delay((int)timeout.TotalMilliseconds, cts.Token))) + { + cts.Cancel(); + return await task; + } + else + { + throw new TimeoutException(); + } + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/DNS/project.json b/MailServer/MailServer/DNS/project.json new file mode 100644 index 0000000..534e923 --- /dev/null +++ b/MailServer/MailServer/DNS/project.json @@ -0,0 +1,52 @@ +{ + "version": "1.0.0-*", + "authors": [ "MichaCo" ], + "packOptions": { + "licenseUrl": "https://github.com/MichaCo/DnsClient.NET/blob/master/LICENSE", + "projectUrl": "https://github.com/MichaCo/DnsClient.NET", + "repository": { + "type": "git", + "url": "https://github.com/MichaCo/DnsClient.NET" + }, + "summary": "A simple DNS client written in C#.", + "tags": [ "DNS", "Name Server", "CSharp", ".NET", ".NET Core" ] + }, + "buildOptions": { + "allowUnsafe": false, + "languageVersion": "csharp6", + "warningsAsErrors": false, + "xmlDoc": true, + "keyFile": "../../Tools/key.snk" + }, + "copyright": "Copyright (c) 2016 MichaConrad", + "configurations": { + "Debug": { + "buildOptions": { + "define": [ "DEBUG" ] + } + }, + "Release": { + "buildOptions": { + "define": [ "RELEASE" ], + "optimize": true + } + } + }, + "dependencies": { + }, + "frameworks": { + "netstandard1.3": { + "dependencies": { + "NETStandard.Library": "1.6.0", + "System.Net.NameResolution": "4.0.0", + "System.Net.NetworkInformation": "4.3.0", + "System.Runtime": "4.3.0" + }, + "buildOptions": { "define": [ "XPLAT" ] } + }, + "net45": { + "frameworkAssemblies": { + } + } + } +} diff --git a/SMTPServer/SMTPServer/DNSClient/DnsHelpers.cs b/MailServer/MailServer/DNSClient/DnsHelpers.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/DnsHelpers.cs rename to MailServer/MailServer/DNSClient/DnsHelpers.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Enums/NsClass.cs b/MailServer/MailServer/DNSClient/Enums/NsClass.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Enums/NsClass.cs rename to MailServer/MailServer/DNSClient/Enums/NsClass.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Enums/NsFlags.cs b/MailServer/MailServer/DNSClient/Enums/NsFlags.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Enums/NsFlags.cs rename to MailServer/MailServer/DNSClient/Enums/NsFlags.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Enums/NsType.cs b/MailServer/MailServer/DNSClient/Enums/NsType.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Enums/NsType.cs rename to MailServer/MailServer/DNSClient/Enums/NsType.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Enums/TcpServices.cs b/MailServer/MailServer/DNSClient/Enums/TcpServices.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Enums/TcpServices.cs rename to MailServer/MailServer/DNSClient/Enums/TcpServices.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Enums/UdpServices.cs b/MailServer/MailServer/DNSClient/Enums/UdpServices.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Enums/UdpServices.cs rename to MailServer/MailServer/DNSClient/Enums/UdpServices.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Query/DnsQueryBase.cs b/MailServer/MailServer/DNSClient/Query/DnsQueryBase.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Query/DnsQueryBase.cs rename to MailServer/MailServer/DNSClient/Query/DnsQueryBase.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Query/DnsQueryRequest.cs b/MailServer/MailServer/DNSClient/Query/DnsQueryRequest.cs similarity index 80% rename from SMTPServer/SMTPServer/DNSClient/Query/DnsQueryRequest.cs rename to MailServer/MailServer/DNSClient/Query/DnsQueryRequest.cs index f781111..858e4e9 100644 --- a/SMTPServer/SMTPServer/DNSClient/Query/DnsQueryRequest.cs +++ b/MailServer/MailServer/DNSClient/Query/DnsQueryRequest.cs @@ -38,12 +38,12 @@ using System.IO; using System.Net; using System.Net.Sockets; using System.Net.NetworkInformation; -using System.Security.Permissions; using System.Text; using DnDns.Enums; using DnDns.Records; using DnDns.Security; +using System.Threading.Tasks; namespace DnDns.Query { @@ -54,8 +54,6 @@ namespace DnDns.Query { private static Random r = new Random(); - private DnsPermission _dnsPermissions; - private int _bytesSent = 0; private int _socketTimeout = 5000; @@ -80,8 +78,6 @@ namespace DnDns.Query #region Constructors public DnsQueryRequest() { - _dnsPermissions = new DnsPermission(PermissionState.Unrestricted); - // Construct the class with some defaults _transactionId = (ushort) r.Next(); _flags = 0; @@ -151,16 +147,16 @@ namespace DnDns.Query /// /// /// - public DnsQueryResponse Resolve(string host, NsType queryType, NsClass queryClass, ProtocolType protocol) + public async Task ResolveAsync(string host, NsType queryType, NsClass queryClass, ProtocolType protocol) { - return Resolve(host, queryType, queryClass, protocol, null); + return await ResolveAsync(host, queryType, queryClass, protocol, null); } - public DnsQueryResponse Resolve(string host, NsType queryType, NsClass queryClass, ProtocolType protocol, TsigMessageSecurityProvider provider) + public async Task ResolveAsync(string host, NsType queryType, NsClass queryClass, ProtocolType protocol, TsigMessageSecurityProvider provider) { string dnsServer = string.Empty; - - // Test for Unix/Linux OS + dnsServer = "8.8.8.8"; + /*// Test for Unix/Linux OS if (Tools.IsPlatformLinuxUnix()) { dnsServer = Tools.DiscoverUnixDnsServerAddress(); @@ -176,9 +172,8 @@ namespace DnDns.Query if (String.IsNullOrEmpty(dnsServer)) throw new Exception("Couldn't detect local DNS Server."); - - return Resolve(dnsServer, host, queryType, queryClass, protocol, provider); - + */ + return await ResolveAsync(dnsServer, host, queryType, queryClass, protocol, provider); } /// @@ -194,15 +189,12 @@ namespace DnDns.Query /// /// /// - public DnsQueryResponse Resolve(string dnsServer, string host, NsType queryType, NsClass queryClass, ProtocolType protocol, IMessageSecurityProvider messageSecurityProvider) + public async Task ResolveAsync(string dnsServer, string host, NsType queryType, NsClass queryClass, ProtocolType protocol, IMessageSecurityProvider messageSecurityProvider) { - // Do stack walk and Demand all callers have DnsPermission. - _dnsPermissions.Demand(); - byte[] bDnsQuery = this.BuildDnsRequest(host, queryType, queryClass, protocol, messageSecurityProvider); // Connect to DNS server and get the record for the current server. - IPHostEntry ipe = System.Net.Dns.GetHostEntry(dnsServer); + IPHostEntry ipe = await Dns.GetHostEntryAsync(dnsServer); IPAddress ipa = ipe.AddressList[0]; IPEndPoint ipep = new IPEndPoint(ipa, (int)UdpServices.Domain); @@ -226,7 +218,11 @@ namespace DnDns.Query } } - Trace.Assert(recvBytes != null, "Failed to retrieve data from the remote DNS server."); + //Console.Assert(recvBytes != null, "Failed to retrieve data from the remote DNS server."); + if(recvBytes == null) + { + Logging.AddLogMessage(Logging.LoggingType.ERROR, "Failed to retrieve data from the remote DNS server."); + } DnsQueryResponse dnsQR = new DnsQueryResponse(); @@ -238,59 +234,71 @@ namespace DnDns.Query private byte[] ResolveUdp(byte[] bDnsQuery, IPEndPoint ipep) { // UDP messages, data size = 512 octets or less - UdpClient udpClient = new UdpClient(); + //UdpClient udpClient = new UdpClient(); + + Socket udp = new Socket(SocketType.Dgram, ProtocolType.Udp); byte[] recvBytes = null; try { - udpClient.Client.ReceiveTimeout = _socketTimeout; - udpClient.Connect(ipep); - udpClient.Send(bDnsQuery, bDnsQuery.Length); - recvBytes = udpClient.Receive(ref ipep); + udp.ReceiveTimeout = _socketTimeout; + //udpClient.Client.ReceiveTimeout = _socketTimeout; + //udpClient.ConnectAsync(ipep); + udp.Connect(ipep); + //udpClient.Send(bDnsQuery, bDnsQuery.Length); + udp.Send(bDnsQuery); + //recvBytes = udpClient.Receive(ref ipep); + recvBytes = new byte[udp.ReceiveBufferSize]; + udp.Receive(recvBytes); } finally { - udpClient.Close(); + //udpClient.Close(); } return recvBytes; } private static byte[] ResolveTcp(byte[] bDnsQuery, IPEndPoint ipep) { - TcpClient tcpClient = new TcpClient(); + Socket tcp = new Socket(SocketType.Stream, ProtocolType.Tcp); + + //TcpClient tcpClient = new TcpClient(); byte[] recvBytes = null; try { - tcpClient.Connect(ipep); - - NetworkStream netStream = tcpClient.GetStream(); - BinaryReader netReader = new System.IO.BinaryReader(netStream); - - netStream.Write(bDnsQuery, 0, bDnsQuery.Length); + tcp.Connect(ipep); + //tcpClient.ConnectAsync(ipep); + //NetworkStream ns = tcp.Receive + //NetworkStream netStream = tcpClient.GetStream(); + //BinaryReader netReader = new System.IO.BinaryReader(netStream); + //netStream.Write(bDnsQuery, 0, bDnsQuery.Length); + tcp.Send(bDnsQuery); // wait until data is avail - while (!netStream.DataAvailable) ; + //while (!netStream.DataAvailable) ; + while (tcp.Available < 1) ; - if (tcpClient.Connected && netStream.DataAvailable) + //if (tcpClient.Connected && netStream.DataAvailable) + if (tcp.Connected && tcp.Available > 1) { // Read first two bytes to find out the length of the response byte[] bLen = new byte[2]; - + // NOTE: The order of the next two lines matter. Do not reorder // Array indexes are also intentionally reversed - bLen[1] = (byte)netStream.ReadByte(); - bLen[0] = (byte)netStream.ReadByte(); + tcp.Receive(bLen, bLen.Length, SocketFlags.None); + //bLen[1] = (byte)netStream.ReadByte(); + //bLen[0] = (byte)netStream.ReadByte(); UInt16 length = BitConverter.ToUInt16(bLen, 0); recvBytes = new byte[length]; - netStream.Read(recvBytes, 0, length); + tcp.Receive(recvBytes, length, SocketFlags.None); + //netStream.Read(recvBytes, 0, length); } - } - finally - { - tcpClient.Close(); + } catch (Exception e) { + Logging.AddException(e); } return recvBytes; } @@ -369,7 +377,7 @@ namespace DnDns.Query memoryStream.Write(data, 0, data.Length); } - Trace.WriteLine(String.Format("The message bytes: {0}", DnsHelpers.DumpArrayToString(memoryStream.ToArray()))); + Logging.AddLogMessage(Logging.LoggingType.INFO, String.Format("The message bytes: {0}", DnsHelpers.DumpArrayToString(memoryStream.ToArray()))); return memoryStream.ToArray(); } diff --git a/SMTPServer/SMTPServer/DNSClient/Query/DnsQueryResponse.cs b/MailServer/MailServer/DNSClient/Query/DnsQueryResponse.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Query/DnsQueryResponse.cs rename to MailServer/MailServer/DNSClient/Query/DnsQueryResponse.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/ARecord.cs b/MailServer/MailServer/DNSClient/Records/ARecord.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/ARecord.cs rename to MailServer/MailServer/DNSClient/Records/ARecord.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/AaaaRecord.cs b/MailServer/MailServer/DNSClient/Records/AaaaRecord.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/AaaaRecord.cs rename to MailServer/MailServer/DNSClient/Records/AaaaRecord.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/AfsdbRecord.cs b/MailServer/MailServer/DNSClient/Records/AfsdbRecord.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/AfsdbRecord.cs rename to MailServer/MailServer/DNSClient/Records/AfsdbRecord.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/AtmaRecord.cs b/MailServer/MailServer/DNSClient/Records/AtmaRecord.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/AtmaRecord.cs rename to MailServer/MailServer/DNSClient/Records/AtmaRecord.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/BaseDnsRecord.cs b/MailServer/MailServer/DNSClient/Records/BaseDnsRecord.cs similarity index 98% rename from SMTPServer/SMTPServer/DNSClient/Records/BaseDnsRecord.cs rename to MailServer/MailServer/DNSClient/Records/BaseDnsRecord.cs index b0aec06..701cbe3 100644 --- a/SMTPServer/SMTPServer/DNSClient/Records/BaseDnsRecord.cs +++ b/MailServer/MailServer/DNSClient/Records/BaseDnsRecord.cs @@ -335,4 +335,12 @@ namespace DnDns.Records #endregion } + + internal class Trace + { + public static void WriteLine(string message) + { + Logging.AddLogMessage(Logging.LoggingType.INFO, message); + } + } } diff --git a/SMTPServer/SMTPServer/DNSClient/Records/CNameRecord.cs b/MailServer/MailServer/DNSClient/Records/CNameRecord.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/CNameRecord.cs rename to MailServer/MailServer/DNSClient/Records/CNameRecord.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/HInfoRecord.cs b/MailServer/MailServer/DNSClient/Records/HInfoRecord.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/HInfoRecord.cs rename to MailServer/MailServer/DNSClient/Records/HInfoRecord.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/IDnsRecord.cs b/MailServer/MailServer/DNSClient/Records/IDnsRecord.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/IDnsRecord.cs rename to MailServer/MailServer/DNSClient/Records/IDnsRecord.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/IsdnRecord.cs b/MailServer/MailServer/DNSClient/Records/IsdnRecord.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/IsdnRecord.cs rename to MailServer/MailServer/DNSClient/Records/IsdnRecord.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/LocRecord.cs b/MailServer/MailServer/DNSClient/Records/LocRecord.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/LocRecord.cs rename to MailServer/MailServer/DNSClient/Records/LocRecord.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/MInfoRecord.cs b/MailServer/MailServer/DNSClient/Records/MInfoRecord.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/MInfoRecord.cs rename to MailServer/MailServer/DNSClient/Records/MInfoRecord.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/MbRecord.cs b/MailServer/MailServer/DNSClient/Records/MbRecord.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/MbRecord.cs rename to MailServer/MailServer/DNSClient/Records/MbRecord.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/MgRecord.cs b/MailServer/MailServer/DNSClient/Records/MgRecord.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/MgRecord.cs rename to MailServer/MailServer/DNSClient/Records/MgRecord.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/MrRecord.cs b/MailServer/MailServer/DNSClient/Records/MrRecord.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/MrRecord.cs rename to MailServer/MailServer/DNSClient/Records/MrRecord.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/MxRecord.cs b/MailServer/MailServer/DNSClient/Records/MxRecord.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/MxRecord.cs rename to MailServer/MailServer/DNSClient/Records/MxRecord.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/NsRecord.cs b/MailServer/MailServer/DNSClient/Records/NsRecord.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/NsRecord.cs rename to MailServer/MailServer/DNSClient/Records/NsRecord.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/PtrRecord.cs b/MailServer/MailServer/DNSClient/Records/PtrRecord.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/PtrRecord.cs rename to MailServer/MailServer/DNSClient/Records/PtrRecord.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/RecordFactory.cs b/MailServer/MailServer/DNSClient/Records/RecordFactory.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/RecordFactory.cs rename to MailServer/MailServer/DNSClient/Records/RecordFactory.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/RecordHeader.cs b/MailServer/MailServer/DNSClient/Records/RecordHeader.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/RecordHeader.cs rename to MailServer/MailServer/DNSClient/Records/RecordHeader.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/RpRecord.cs b/MailServer/MailServer/DNSClient/Records/RpRecord.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/RpRecord.cs rename to MailServer/MailServer/DNSClient/Records/RpRecord.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/RtRecord.cs b/MailServer/MailServer/DNSClient/Records/RtRecord.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/RtRecord.cs rename to MailServer/MailServer/DNSClient/Records/RtRecord.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/SoaRecord.cs b/MailServer/MailServer/DNSClient/Records/SoaRecord.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/SoaRecord.cs rename to MailServer/MailServer/DNSClient/Records/SoaRecord.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/SrvRecord.cs b/MailServer/MailServer/DNSClient/Records/SrvRecord.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/SrvRecord.cs rename to MailServer/MailServer/DNSClient/Records/SrvRecord.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/TSigRecord.cs b/MailServer/MailServer/DNSClient/Records/TSigRecord.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/TSigRecord.cs rename to MailServer/MailServer/DNSClient/Records/TSigRecord.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/TxtRecord.cs b/MailServer/MailServer/DNSClient/Records/TxtRecord.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/TxtRecord.cs rename to MailServer/MailServer/DNSClient/Records/TxtRecord.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/UnknownRecord.cs b/MailServer/MailServer/DNSClient/Records/UnknownRecord.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/UnknownRecord.cs rename to MailServer/MailServer/DNSClient/Records/UnknownRecord.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/WksRecord.cs b/MailServer/MailServer/DNSClient/Records/WksRecord.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/WksRecord.cs rename to MailServer/MailServer/DNSClient/Records/WksRecord.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Records/X25Record.cs b/MailServer/MailServer/DNSClient/Records/X25Record.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Records/X25Record.cs rename to MailServer/MailServer/DNSClient/Records/X25Record.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Security/IMessageSecurityProvider.cs b/MailServer/MailServer/DNSClient/Security/IMessageSecurityProvider.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Security/IMessageSecurityProvider.cs rename to MailServer/MailServer/DNSClient/Security/IMessageSecurityProvider.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Security/TsigMessageSecurityProvider.cs b/MailServer/MailServer/DNSClient/Security/TsigMessageSecurityProvider.cs similarity index 100% rename from SMTPServer/SMTPServer/DNSClient/Security/TsigMessageSecurityProvider.cs rename to MailServer/MailServer/DNSClient/Security/TsigMessageSecurityProvider.cs diff --git a/SMTPServer/SMTPServer/DNSClient/Tools.cs b/MailServer/MailServer/DNSClient/Tools.cs similarity index 81% rename from SMTPServer/SMTPServer/DNSClient/Tools.cs rename to MailServer/MailServer/DNSClient/Tools.cs index 6fb99b1..1359651 100644 --- a/SMTPServer/SMTPServer/DNSClient/Tools.cs +++ b/MailServer/MailServer/DNSClient/Tools.cs @@ -39,6 +39,7 @@ using System.Net.NetworkInformation; using System.Net.Sockets; using DnDns.Enums; +using System.Runtime.InteropServices; namespace DnDns { @@ -66,20 +67,11 @@ namespace DnDns /// Tests to see if this is running on a Linux or Unix Platform /// /// true if unix or linux is detected - //public static bool IsPlatformLinuxUnix() - //{ - // int p = (int)Environment.OSVersion.Platform; - - // Test for Unix / Linux OS - // if (p == 4 || p == 128) - // { - // return true; - // } - // else - // { - // return false; - // } - //} + public static bool IsPlatformLinuxUnix() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return false; + return true; + } /// /// Look up the port name for any given port number. @@ -149,36 +141,36 @@ namespace DnDns } return string.Empty; } + + //public static IPAddressCollection DiscoverDnsServerAddresses() + //{ + // NetworkInterface[] arrNetworkInterfaces = NetworkInterface.GetAllNetworkInterfaces(); - public static IPAddressCollection DiscoverDnsServerAddresses() - { - NetworkInterface[] arrNetworkInterfaces = NetworkInterface.GetAllNetworkInterfaces(); + // foreach (NetworkInterface objNetworkInterface in arrNetworkInterfaces) + // { + // if ( + // (objNetworkInterface.OperationalStatus == OperationalStatus.Up) && + // (objNetworkInterface.Speed > 0) && + // (objNetworkInterface.NetworkInterfaceType != NetworkInterfaceType.Loopback) && + // (objNetworkInterface.NetworkInterfaceType != NetworkInterfaceType.Tunnel) + // ) + // { + // IPAddressCollection candidate = objNetworkInterface.GetIPProperties().DnsAddresses; + // bool found = false; + // foreach (IPAddress addr in candidate) + // { + // // Make this collection contains IPv4 addresses only + // if (addr.AddressFamily == AddressFamily.InterNetwork) + // found = true; + // } - foreach (NetworkInterface objNetworkInterface in arrNetworkInterfaces) - { - if ( - (objNetworkInterface.OperationalStatus == OperationalStatus.Up) && - (objNetworkInterface.Speed > 0) && - (objNetworkInterface.NetworkInterfaceType != NetworkInterfaceType.Loopback) && - (objNetworkInterface.NetworkInterfaceType != NetworkInterfaceType.Tunnel) - ) - { - IPAddressCollection candidate = objNetworkInterface.GetIPProperties().DnsAddresses; - bool found = false; - foreach (IPAddress addr in candidate) - { - // Make this collection contains IPv4 addresses only - if (addr.AddressFamily == AddressFamily.InterNetwork) - found = true; - } + // if (found) + // return candidate; + // } + // } - if (found) - return candidate; - } - } - - return null; - } + // return null; + //} //public static uint ByteToUInt(byte[] theByte) //{ diff --git a/MailServer/MailServer/Database.cs b/MailServer/MailServer/Database.cs new file mode 100644 index 0000000..e2d2d42 --- /dev/null +++ b/MailServer/MailServer/Database.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace SMTPServer +{ + public class MysqlDB : DbContext + { + const string Hostname = "192.168.178.148"; + const string DatabaseName = "vmail"; + const string Uid = "root"; + const string Pwd = "fabian.11"; + + public DbSet Domains { get; set; } + + public DbSet Accounts { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseMySql(@"Server=" + Hostname + ";database=" + DatabaseName + ";uid=" + Uid + ";pwd=" + Pwd + ";"); + } + + public class Domains + { + public int Id { get; set; } + public string Domain { get; set; } + public bool IsMail { get; set; } + } + + public class Accounts + { + public int Id { get; set; } + public string Name { get; set; } + public int Domain { get; set; } + public string Password { get; set; } + public bool Active { get; set; } + } +} \ No newline at end of file diff --git a/SMTPServer/SMTPServer/Exceptions/NoMailsInQueueException.cs b/MailServer/MailServer/Exceptions/NoMailsInQueueException.cs similarity index 100% rename from SMTPServer/SMTPServer/Exceptions/NoMailsInQueueException.cs rename to MailServer/MailServer/Exceptions/NoMailsInQueueException.cs diff --git a/SMTPServer/SMTPServer/Exceptions/PortUsedException.cs b/MailServer/MailServer/Exceptions/PortUsedException.cs similarity index 100% rename from SMTPServer/SMTPServer/Exceptions/PortUsedException.cs rename to MailServer/MailServer/Exceptions/PortUsedException.cs diff --git a/MailServer/MailServer/GetDNSEntries.cs b/MailServer/MailServer/GetDNSEntries.cs new file mode 100644 index 0000000..8826d57 --- /dev/null +++ b/MailServer/MailServer/GetDNSEntries.cs @@ -0,0 +1,71 @@ +using DnsClient; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace SMTPServer +{ + public class GetDNSEntries + { + public static async System.Threading.Tasks.Task GetMXRecordAsync(string domain) => await GetMXRecordAsync(domain, 0); + + public static async System.Threading.Tasks.Task GetMXRecordAsync(string domain, int nr) + { + var lookup = new LookupClient(); + var result = await lookup.QueryAsync(domain, QueryType.MX); + + var records = result.AllRecords.ToArray(); + + //ToDo priority sorting + + var rec = new string[records.Length]; + + for(int i = 0; i < records.Length; i++) + { + var str = records[i].RecordToString(); + var s = str.Split(' '); + if (s.Length == 1) + { + rec[i] = s[0]; + } else if (s.Length == 2) + { + rec[i] = s[1]; + } else + { + rec[i] = null; + } + } + + return rec; + /* + foreach (var r in result.AllRecords) + { + Console.WriteLine(r.RecordToString()); + }*/ + } + + public static async System.Threading.Tasks.Task GetIPAddressFromDnsArrayAsync(string[] dnsnames) + { + var ips = new string[dnsnames.Length]; + for(int i = 0; i < dnsnames.Length; i++) + { + ips[i] = await GetIpFromDnsNameAsync(dnsnames[i]); + } + + return ips; + } + + public static async System.Threading.Tasks.Task GetIpFromDnsNameAsync(string dnsname) + { + var lookup = new LookupClient(); + var result = await lookup.QueryAsync(dnsname, QueryType.A); + + var records = result.AllRecords.ToArray(); + + if (records.Length < 1) return null; + return records[0].RecordToString(); + } + } +} \ No newline at end of file diff --git a/MailServer/MailServer/GlobalSuppressions.cs b/MailServer/MailServer/GlobalSuppressions.cs new file mode 100644 index 0000000..e7c317c --- /dev/null +++ b/MailServer/MailServer/GlobalSuppressions.cs @@ -0,0 +1,20 @@ + +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE1006:Benennungsstile", Justification = "", Scope = "member", Target = "~P:SMTPServer.MailQueue.QueueMail._Count")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE1006:Benennungsstile", Justification = "", Scope = "member", Target = "~P:SMTPServer.MailQueue.QueueMail._Mail")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE1006:Benennungsstile", Justification = "", Scope = "member", Target = "~P:SMTPServer.MailQueue.QueueMail._QueueEntered")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE1006:Benennungsstile", Justification = "", Scope = "member", Target = "~P:SMTPServer.MailQueue.QueueMail._DestinationIps")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE1006:Benennungsstile", Justification = "", Scope = "member", Target = "~P:SMTPServer.MailQueue.QueueMail._DestinationDnsNames")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE1006:Benennungsstile", Justification = "", Scope = "member", Target = "~P:SMTPServer.Mail._From")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE1006:Benennungsstile", Justification = "", Scope = "member", Target = "~P:SMTPServer.Mail._To")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE1006:Benennungsstile", Justification = "", Scope = "member", Target = "~P:SMTPServer.Mail._Subject")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE1006:Benennungsstile", Justification = "", Scope = "member", Target = "~P:SMTPServer.Mail._Others")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE1006:Benennungsstile", Justification = "", Scope = "member", Target = "~P:SMTPServer.Mail._Date")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE1006:Benennungsstile", Justification = "", Scope = "member", Target = "~P:SMTPServer.Mail._MessageId")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE1006:Benennungsstile", Justification = "", Scope = "member", Target = "~P:SMTPServer.Mail._MIME_Version")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Stil", "IDE1006:Benennungsstile", Justification = "", Scope = "member", Target = "~P:SMTPServer.MailQueue.QueueMail._DestinationIndex")] + diff --git a/SMTPServer/Logging/Logging.cs b/MailServer/MailServer/Logging/Logging.cs similarity index 77% rename from SMTPServer/Logging/Logging.cs rename to MailServer/MailServer/Logging/Logging.cs index afdd261..8535e9f 100644 --- a/SMTPServer/Logging/Logging.cs +++ b/MailServer/MailServer/Logging/Logging.cs @@ -1,7 +1,7 @@ using System; -namespace Logging -{ +//namespace Logging +//{ public class Logging { private static LoggingType[] _ActiveLoggingTypes { get; set; } @@ -16,15 +16,18 @@ namespace Logging ALL } - - - private static void AddLogMessage(LoggingType messagetype, string message) + public static void AddLogMessage(LoggingType messagetype, string message) { if(CheckLoggingType(messagetype)) PrintToConsole(messagetype, message); } + public static void AddException(Exception e) + { + AddLogMessage(LoggingType.ERROR, e.Message); + } + private static bool CheckLoggingType(LoggingType lt){ - if (_ActiveLoggingTypes.Length < 1) { + if (_ActiveLoggingTypes == null ||_ActiveLoggingTypes.Length < 1) { _ActiveLoggingTypes = new LoggingType[1]; _ActiveLoggingTypes[0] = LoggingType.ALL; } @@ -47,4 +50,4 @@ namespace Logging Console.WriteLine("[" + lt.ToString() + "]" + message); } } -} +//} diff --git a/MailServer/MailServer/MTACommands.cs b/MailServer/MailServer/MTACommands.cs new file mode 100644 index 0000000..aa5d791 --- /dev/null +++ b/MailServer/MailServer/MTACommands.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace SMTPServer +{ + public class MTACommandsDict : Dictionary + { + public MTACommandsDict() + { + Add(MTACommands.HELO, "HELO"); + Add(MTACommands.DATA, "DATA"); + Add(MTACommands.EXPN, "EXPN"); + Add(MTACommands.HELP, "HELP"); + Add(MTACommands.MAIL_FROM, "MAIL FROM"); + Add(MTACommands.NOOP, "NOOP"); + Add(MTACommands.QUIT, "QUIT"); + Add(MTACommands.RCPT_TO, "RCPT TO"); + Add(MTACommands.RSET, "RSET"); + Add(MTACommands.SAML_FROM, "SAML FROM"); + Add(MTACommands.SEND_FROM, "SEND FROM"); + Add(MTACommands.SOML_FROM, "SOML FROM"); + Add(MTACommands.TURN, "TURN"); + Add(MTACommands.VERB, "VERB"); + Add(MTACommands.VRFY, "VRFY"); + Add(MTACommands.ATRN, "ATRN"); + Add(MTACommands.AUTH, "AUTH"); + Add(MTACommands.BDAT, "BDAT"); + Add(MTACommands.EHLO, "EHLO"); + Add(MTACommands.ETRN, "ETRN"); + Add(MTACommands.RCPT, "RCPT"); + Add(MTACommands.SAML, "SAML"); + Add(MTACommands.SEND, "SEND"); + Add(MTACommands.SOML, "SOML"); + Add(MTACommands.STARTTL, "STARTTLs"); + Add(MTACommands.AUTH_LOGIN, "AUTH LOGIN"); + } + } + + public enum MTACommands + { + HELO, + MAIL_FROM, + RCPT_TO, + DATA, + RSET, + QUIT, + HELP, + VRFY, + EXPN, + VERB, + NOOP, + TURN, + SEND_FROM, + SOML_FROM, + SAML_FROM, + ATRN, + AUTH, + BDAT, + EHLO, + ETRN, + RCPT, + SAML, + SEND, + SOML, + STARTTL, + AUTH_LOGIN + } + + public enum ResponseCodes + { + C211 = 211, + C214 = 214, + C220 = 220, + C221 = 221, + C250 = 250, + C251 = 251, + C252 = 252, + C253 = 253, + C334 = 334, + C354 = 354, + C355 = 355, + C421 = 421, + C432 = 432, + C450 = 450, + C451 = 451, + C452 = 452, + C453 = 453, + C454 = 454, + C458 = 458, + C459 = 459, + C500 = 500, + C501 = 501, + C502 = 502, + C503 = 503, + C504 = 504, + C521 = 521, + C530 = 530, + C534 = 534, + C538 = 538, + C550 = 550, + C551 = 551, + C552 = 552, + C553 = 553, + C554 = 554 + } +} \ No newline at end of file diff --git a/MailServer/MailServer/MTASession.cs b/MailServer/MailServer/MTASession.cs new file mode 100644 index 0000000..d86b4df --- /dev/null +++ b/MailServer/MailServer/MTASession.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; + +namespace SMTPServer +{ + class MTASession + { + private static MTACommandsDict MTACommandsDict = new MTACommandsDict(); + private ConnectedEventArgs ConnectedEventArgs { get; set; } + + private bool Initialized { get; set; } + private string SenderHostname { get; set; } + + private bool AuthActive { get; set; } + private string Username { get; set; } + private string Password { get; set; } + private bool Authenticated { get; set; } + + private bool DATA { get; set; } + + private string Mail_From { get; set; } + + public MTASession(ConnectedEventArgs e) + { + ConnectedEventArgs = e; + Thread th = new Thread(new ThreadStart(StartCommunication)); + th.Start(); + } + + private void StartCommunication() + { + ConnectedEventArgs.SendResponse(ResponseCodes.C220, MailTransferAgent.Hostname); + ConnectedEventArgs.NewLine += ConnectedEventArgs_NewLine; + } + + private void ConnectedEventArgs_NewLine(string line) + { + if (line.StartsWith(MTACommandsDict[MTACommands.RSET])) + { + // Schauen was alles resettet wird + } + + if (!Initialized) + { + if (line.StartsWith(MTACommandsDict[MTACommands.HELO])) + { + ConnectedEventArgs.SendResponse(ResponseCodes.C250, MailTransferAgent.Hostname); + //} else if (line.StartsWith(MTACommandsDict[MTACommands.EHLO])) { + // ConnectedEventArgs.SendResponse(ResponseCodes.C250, MailTransferAgent.Hostname); + } + else + { + ConnectedEventArgs.SendResponse(ResponseCodes.C503, MailTransferAgent.Hostname); + return; + } + Initialized = true; + } + + if (line.StartsWith(MTACommandsDict[MTACommands.AUTH_LOGIN])) + { + AuthActive = true; + ConnectedEventArgs.SendResponse(ResponseCodes.C334); + } + else if (line.StartsWith(MTACommandsDict[MTACommands.MAIL_FROM])) + { + var args = line.Split(':'); + if (args.Length < 2) + { + ConnectedEventArgs.SendResponse(ResponseCodes.C501); + return; + } + var mailFrom = args[1]; + mailFrom.Replace('<', ' '); + } + else if (line.StartsWith(MTACommandsDict[MTACommands.QUIT])) + { + //throw new NotImplementedException(); + } else + { + ConnectedEventArgs.SendResponse(ResponseCodes.C500); + } + } + } +} diff --git a/MailServer/MailServer/Mail.cs b/MailServer/MailServer/Mail.cs new file mode 100644 index 0000000..19994f5 --- /dev/null +++ b/MailServer/MailServer/Mail.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace SMTPServer +{ + public class Mail + { + public string To { get; private set; } + public string From { get; private set; } + + public string Subject { get; private set; } + public string MessageId { get; private set; } + public string Date { get; private set; } + + public string MIME_Version { get; private set; } + + public string Others { get; private set; } + + + public Mail(string mail) + { + + } + + public string GetSenderDomain() + { + return From.Split('@')[1]; + } + } +} diff --git a/MailServer/MailServer/MailQueue.cs b/MailServer/MailServer/MailQueue.cs new file mode 100644 index 0000000..5d442fe --- /dev/null +++ b/MailServer/MailServer/MailQueue.cs @@ -0,0 +1,87 @@ +using DnsClient; +using SMTPServer.Exceptions; +using System; +using System.Collections.Generic; + +namespace SMTPServer +{ + public class MailQueue + { + private static List Mails = null; + + public static async void AddToMesssageQueueAsync(Mail mail) + { + var qmail = new QueueMail(mail); + + qmail.DestinationDnsNames = await GetDNSEntries.GetMXRecordAsync(mail.GetSenderDomain()); + qmail.DestinationIps = await GetDNSEntries.GetIPAddressFromDnsArrayAsync(qmail.DestinationDnsNames); + + lock (Mails) + { + if (Mails == null) + { + Mails = new List(); + } + Mails.Add(qmail); + } + } + + public static QueueMail GetNextMail() + { + lock (Mails) + { + if(Mails.Count < 1) + { + throw new NoMailsInQueueException(); + } + + var m = Mails[0]; + + Mails.Remove(m); + m.Count++; + Mails.Add(m); + + return m; + } + } + + public static void RemoveMailFromQueue(QueueMail mail) + { + lock (Mails) + { + Mails.Remove(mail); + } + } + + public class QueueMail + { + public DateTime QueueEntered { get; private set; } + public int Count { get; set; } + public Mail Mail { get; private set; } + public string[] DestinationIps { get; set; } + public string[] DestinationDnsNames { get; set; } + public int DestinationIndex { get; set; } + + public QueueMail (Mail mail) + { + Mail = mail; + QueueEntered = DateTime.Now; + Count = 0; + } + + public byte[] GetIpBytes() => GetIpBytes(DestinationIndex); + + public byte[] GetIpBytes(int index) + { + var parts = DestinationIps[index].Split('.'); + var bytes = new byte[parts.Length]; + for(int i = 0; i < parts.Length; i++) + { + bytes[i] = byte.Parse(parts[i]); + } + + return bytes; + } + } + } +} diff --git a/SMTPServer/SMTPServer/SMTPServer.csproj b/MailServer/MailServer/MailServer.csproj similarity index 56% rename from SMTPServer/SMTPServer/SMTPServer.csproj rename to MailServer/MailServer/MailServer.csproj index 576f706..5cc3a31 100644 --- a/SMTPServer/SMTPServer/SMTPServer.csproj +++ b/MailServer/MailServer/MailServer.csproj @@ -1,7 +1,8 @@ - + 1701;1702;1705;1006 + AnyCPU Exe @@ -12,6 +13,25 @@ + + + + + + + + + 1.0.0-beta-1011 + + + 1.0.0-preview2-1-003177 + + + 1.1.0-preview4-final + + + 1.1.0-preview4-final + 5.0.2 @@ -31,6 +51,12 @@ 1.1.0 + + 1.1.0-rtm-10012 + + + 1.1.0-rtm-10012 + 4.0.1 @@ -38,5 +64,6 @@ 4.3.0 + \ No newline at end of file diff --git a/MailServer/MailServer/MailTransferAgent.cs b/MailServer/MailServer/MailTransferAgent.cs new file mode 100644 index 0000000..d062a28 --- /dev/null +++ b/MailServer/MailServer/MailTransferAgent.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using SMTPServer.Exceptions; +using System.Net; + +namespace SMTPServer +{ + class MailTransferAgent + { + public const string Hostname = "mail.fstamm.net"; + private static MTACommandsDict MTACommandsDict = new MTACommandsDict(); + private static Thread Thread; + private static List Sessions { get; set; } + + public static void StartMailTransferAgent() + { + Thread = new Thread(new ThreadStart(MTA)); + Thread.Start(); + + var portListener = new PortListener(25); + portListener.OnConnected += PortListener_OnConnected; + } + + private static void PortListener_OnConnected(object source, ConnectedEventArgs e) + { + var session = new MTASession(e); + lock (Sessions) + { + Sessions.Add(session); + } + //throw new NotImplementedException(); + } + + public static void MTA() + { + while (true) + { + MailQueue.QueueMail mail = null; + try + { + MailQueue.GetNextMail(); + } + catch (NoMailsInQueueException) + { + Thread.Sleep(100); + continue; + } + + var charset = Encoding.UTF8; + var client = new StartTcpConnection(25, new IPAddress(mail.GetIpBytes()), charset); + + if (!client.Connected) ; //ToDo Errorfall + + + + } + } + + public static string WaitForResponseCode(ResponseCodes code) + { + + return null; + } + } +} diff --git a/MailServer/MailServer/PortListener.cs b/MailServer/MailServer/PortListener.cs new file mode 100644 index 0000000..00bc4b0 --- /dev/null +++ b/MailServer/MailServer/PortListener.cs @@ -0,0 +1,125 @@ +using SMTPServer.Exceptions; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; + +namespace SMTPServer +{ + public delegate void ConnectedEventHandler(object source, ConnectedEventArgs e); + + public class ConnectedEventArgs : EventArgs + { + public delegate void ReceivedLineEventHandler(string line); + + public event ReceivedLineEventHandler NewLine; + + private Encoding Encoding { get; set; } + private string Others { get; set; } + public TcpClient Client { get; private set; } + + private NetworkStream Stream { get; set; } + public ConnectedEventArgs(TcpClient client, Encoding encoding) + { + Client = client; + Encoding = encoding; + Stream = client.GetStream(); + } + + public void SendResponse(ResponseCodes responseCode) => SendResponse(responseCode, ""); + + public void SendResponse (ResponseCodes responseCode, string args) + { + var text = ((int)responseCode).ToString(); + text += " " + args; + var bytes = Encoding.UTF8.GetBytes(text); + Stream.Write(bytes, 0 , bytes.Length); + } + + private void ReadClientInput() + { + while (true) + { + if (Stream.DataAvailable) + { + byte[] buffer = new byte[Client.ReceiveBufferSize]; + Stream.Read(buffer, 0, buffer.Length); + + var str = Encoding.GetString(buffer); + lock (Others) + { + Others += str; + } + + CheckLines(); + } else + { + Thread.Sleep(10); + } + } + } + + private void CheckLines() + { + lock (Others) + { + var line = ""; + foreach (char c in Others) + { + if (c.Equals('\n')) + { + Console.WriteLine(line); + NewLine(line); + line = ""; + } + else line += c; + } + Others = line; + } + } + } + + public class PortListener + { + static List _Ports = null; + TcpListener _Listener; + + public event ConnectedEventHandler OnConnected; + + public PortListener(int port) + { + if(_Ports == null) + { + _Ports = new List(); + } + + foreach(var i in _Ports) + { + if (i == port) + { + throw new PortUsedException(); + } + } + + _Ports.Add(port); + StartListening(new TcpListener(IPAddress.Any, port)); + } + + private void StartListening(TcpListener listener) + { + listener.Start(); + _Listener = listener; + Thread thread = new Thread(new ThreadStart(ListenerAsync)); + thread.Start(); + } + + private async void ListenerAsync() + { + var client = await _Listener.AcceptTcpClientAsync(); + Console.WriteLine("Connection"); + OnConnected(this, new ConnectedEventArgs(client, Encoding.UTF8)); + } + } +} diff --git a/MailServer/MailServer/Program.cs b/MailServer/MailServer/Program.cs new file mode 100644 index 0000000..4adfa34 --- /dev/null +++ b/MailServer/MailServer/Program.cs @@ -0,0 +1,119 @@ +using DnsClient; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace SMTPServer +{ + class Program + { + static void Main(string[] args) + { + /*using (var context = new MysqlDB()) + { + context.Database.EnsureCreated(); + + var domain = new Domains() + { + Domain = "HI", + IsMail = true + }; + context.Add(domain); + context.SaveChanges(); + + var res = context.Domains.Find(); + }*/ + + + //Console.WriteLine("####################"); + + /*var request = new DnsQueryRequest(); + var response = request.ResolveAsync("fabianstamm.de", DnDns.Enums.NsType.MX, DnDns.Enums.NsClass.ANY, System.Net.Sockets.ProtocolType.Tcp); + + OutputResults(response);*/ + //var t = new Thread(new ThreadStart(Test)); + //t.Start(); + //TestAsync(); + + var listener = new PortListener(5122); + listener.OnConnected += Listener_OnConnected; + + while (true) + { + Thread.Sleep(500); + + } + //var listener = new PortListener(5122); + //listener.OnConnected += Listener_OnConnected; + } + + private static async void TestAsync() + { + var lookup = new LookupClient(); + var result = await lookup.QueryAsync("fabianstamm.de.", QueryType.MX); + + foreach (var r in result.AllRecords) + { + Console.WriteLine(r.RecordToString()); + } + } + + private static void Listener_OnConnected(object source, ConnectedEventArgs e) + { + var session = new MTASession(e); + //throw new NotImplementedException(); + } + /* + private static async void OutputResults(Task res) + { + OutputResults(await res); + } + + private static void OutputResults(DnDns.Query.DnsQueryResponse response) + { + Console.WriteLine("Bytes received: " + response.BytesReceived); + + Console.WriteLine("Name: " + response.Name); + Console.WriteLine("OpCode: " + response.NsClass); + Console.WriteLine("NsFlags: " + response.NsFlags); + Console.WriteLine("NsType: " + response.NsType); + Console.WriteLine("RCode: " + response.RCode); + Console.WriteLine("OpCode: " + response.OpCode); + + // Enumerate the Answer Records + Console.WriteLine("Answers:"); + foreach (IDnsRecord record in response.Answers) + { + Console.WriteLine(record.Answer); + Console.WriteLine(" |--- RDATA Field Length: " + record.DnsHeader.DataLength); + Console.WriteLine(" |--- Name: " + record.DnsHeader.Name); + Console.WriteLine(" |--- NS Class: " + record.DnsHeader.NsClass); + Console.WriteLine(" |--- NS Type: " + record.DnsHeader.NsType); + Console.WriteLine(" |--- TTL: " + record.DnsHeader.TimeToLive); + Console.WriteLine(); + } + + foreach (IDnsRecord record in response.AuthoritiveNameServers) + { + Console.WriteLine(record.Answer); + Console.WriteLine(" |--- RDATA Field Length: " + record.DnsHeader.DataLength); + Console.WriteLine(" |--- Name: " + record.DnsHeader.Name); + Console.WriteLine(" |--- NS Class: " + record.DnsHeader.NsClass); + Console.WriteLine(" |--- NS Type: " + record.DnsHeader.NsType); + Console.WriteLine(" |--- TTL: " + record.DnsHeader.TimeToLive); + Console.WriteLine(); + } + + foreach (IDnsRecord record in response.AdditionalRRecords) + { + Console.WriteLine(record.Answer); + Console.WriteLine(" |--- RDATA Field Length: " + record.DnsHeader.DataLength); + Console.WriteLine(" |--- Name: " + record.DnsHeader.Name); + Console.WriteLine(" |--- NS Class: " + record.DnsHeader.NsClass); + Console.WriteLine(" |--- NS Type: " + record.DnsHeader.NsType); + Console.WriteLine(" |--- TTL: " + record.DnsHeader.TimeToLive); + Console.WriteLine(); + } + }*/ + } +} \ No newline at end of file diff --git a/MailServer/MailServer/Properties/launchSettings.json b/MailServer/MailServer/Properties/launchSettings.json new file mode 100644 index 0000000..985b3eb --- /dev/null +++ b/MailServer/MailServer/Properties/launchSettings.json @@ -0,0 +1,7 @@ +{ + "profiles": { + "SMTPServer": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/SMTPServer/SMTPServer/StartTcpConnection.cs b/MailServer/MailServer/StartTcpConnection.cs similarity index 63% rename from SMTPServer/SMTPServer/StartTcpConnection.cs rename to MailServer/MailServer/StartTcpConnection.cs index 7b1ca9a..ae6bf99 100644 --- a/SMTPServer/SMTPServer/StartTcpConnection.cs +++ b/MailServer/MailServer/StartTcpConnection.cs @@ -9,9 +9,15 @@ namespace SMTPServer { class StartTcpConnection : Socket { - private List _Lines = new List(); - private string _Others { get; set; } - private Encoding _Encoding { get; set; } + private List Lines = new List(); + private int LinesAvailable { get + { + return Lines.Count; + } + } + + private string Others { get; set; } + private Encoding Encoding { get; set; } public StartTcpConnection(int port, IPAddress destination, Encoding encoding) : base (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) { @@ -23,13 +29,13 @@ namespace SMTPServer { while (true) { - byte[] buffer = new byte[this.ReceiveBufferSize]; + byte[] buffer = new byte[ReceiveBufferSize]; Receive(buffer); - var str = _Encoding.GetString(buffer); - lock (_Others) + var str = Encoding.GetString(buffer); + lock (Others) { - _Others += str; + Others += str; } CheckLines(); @@ -38,30 +44,30 @@ namespace SMTPServer private void CheckLines() { - lock (_Others) + lock (Others) { var line = ""; - foreach (char c in _Others) + foreach (char c in Others) { if (c.Equals('\n')) { - lock (_Lines) + lock (Lines) { - _Lines.Add(line); + Lines.Add(line); line = ""; } } else line += c; } - _Others = line; + Others = line; } } public string GetLine() { - lock (_Lines) + lock (Lines) { - return _Lines[0]; + return Lines[0]; } } } diff --git a/SMTP/Program.cs b/SMTP/Program.cs deleted file mode 100644 index c81448f..0000000 --- a/SMTP/Program.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -class Program -{ - static void Main(string[] args) - { - Console.WriteLine("Hello World!"); - } -} diff --git a/SMTP/SMTP.csproj b/SMTP/SMTP.csproj deleted file mode 100644 index 82c832a..0000000 --- a/SMTP/SMTP.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - - - Exe - netcoreapp1.0 - - - - - - - - - - 1.0.1 - - - 1.0.0-alpha-20161104-2 - All - - - - - diff --git a/SMTPServer/DNSResolver/DNSResolver.csproj b/SMTPServer/DNSResolver/DNSResolver.csproj deleted file mode 100644 index 13c8161..0000000 --- a/SMTPServer/DNSResolver/DNSResolver.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - 1701;1702;1705;1006 - - - Exe - netcoreapp1.0 - - - - - - - - 1.0.1 - - - 1.0.0-alpha-20161104-2 - All - - - - \ No newline at end of file diff --git a/SMTPServer/DNSResolver/DnsHelper.cs b/SMTPServer/DNSResolver/DnsHelper.cs deleted file mode 100644 index 24614ab..0000000 --- a/SMTPServer/DNSResolver/DnsHelper.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System; -using System.Text; - -namespace DNSResolver -{ - internal static class DnsHelpers - { - private const long Epoch = 621355968000000000; - - internal static byte[] CanonicaliseDnsName(string name, bool lowerCase) - { - if (!name.EndsWith(".")) - { - name += "."; - } - - if (name == ".") - { - return new byte[1]; - } - - StringBuilder sb = new StringBuilder(); - - sb.Append('\0'); - - for (int i = 0, j = 0; i < name.Length; i++, j++) - { - if (lowerCase) - { - sb.Append(char.ToLower(name[i])); - } - else - { - sb.Append(name[i]); - } - - if (name[i] == '.') - { - sb[i - j] = (char)(j & 0xff); - j = -1; - } - } - - sb[sb.Length - 1] = '\0'; - - return Encoding.ASCII.GetBytes(sb.ToString()); - } - - internal static String DumpArrayToString(byte[] bytes) - { - StringBuilder builder = new StringBuilder(); - - builder.Append("["); - - foreach (byte b in bytes) - { - builder.Append(" "); - builder.Append((sbyte)b); - builder.Append(" "); - } - - builder.Append("]"); - - return builder.ToString(); - } - - /// - /// Converts a instance of a class to a 48 bit format time since epoch. - /// Epoch is defined as 1-Jan-70 UTC. - /// - /// The instance to convert to DNS format. - /// The upper 16 bits of time. - /// The lower 32 bits of the time object. - internal static void ConvertToDnsTime(DateTime dateTimeToConvert, out int timeHigh, out long timeLow) - { - long secondsFromEpoch = (dateTimeToConvert.ToUniversalTime().Ticks - Epoch) / 10000000; - timeHigh = (int)(secondsFromEpoch >> 32); - timeLow = (secondsFromEpoch & 0xFFFFFFFFL); - - Logging.AddLogMessage(LoggingType.TRACE, "test"); - Trace.WriteLine(String.Format("Date: {0}", dateTimeToConvert)); - Trace.WriteLine(String.Format("secondsFromEpoch: {0}", secondsFromEpoch)); - Trace.WriteLine(String.Format("timeHigh: {0}", timeHigh)); - Trace.WriteLine(String.Format("timeLow: {0}", timeLow)); - } - - /// - /// Convert from DNS 48 but time format to a instance. - /// - /// The upper 16 bits of time. - /// The lower 32 bits of the time object. - /// The converted date time - internal static DateTime ConvertFromDnsTime(long timeLow, long timeHigh) - { - long time = (timeHigh << 32) + timeLow; - time = time * 10000000; - time += Epoch; - - return new DateTime(time); - } - - /// - /// Convert from DNS 48 but time format to a instance. - /// - /// The upper 48 bits of time. - /// The converted date time - internal static DateTime ConvertFromDnsTime(long dnsTime) - { - dnsTime = dnsTime * 10000000; - dnsTime += Epoch; - - return new DateTime(dnsTime); - } - } -} diff --git a/SMTPServer/Logging/Logging.csproj b/SMTPServer/Logging/Logging.csproj deleted file mode 100644 index 550eaa7..0000000 --- a/SMTPServer/Logging/Logging.csproj +++ /dev/null @@ -1,23 +0,0 @@ - - - - 1701;1702;1705;1006 - - - netstandard1.4 - - - - - - - - 1.6 - - - 1.0.0-alpha-20161104-2 - All - - - - \ No newline at end of file diff --git a/SMTPServer/SMTPServer/Logging/Logging.cs b/SMTPServer/SMTPServer/Logging/Logging.cs deleted file mode 100644 index afdd261..0000000 --- a/SMTPServer/SMTPServer/Logging/Logging.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; - -namespace Logging -{ - public class Logging - { - private static LoggingType[] _ActiveLoggingTypes { get; set; } - - public enum LoggingType - { - TRACE, - ERROR, - DEBUG, - WARNING, - INFO, - ALL - } - - - - private static void AddLogMessage(LoggingType messagetype, string message) - { - if(CheckLoggingType(messagetype)) PrintToConsole(messagetype, message); - } - - private static bool CheckLoggingType(LoggingType lt){ - if (_ActiveLoggingTypes.Length < 1) { - _ActiveLoggingTypes = new LoggingType[1]; - _ActiveLoggingTypes[0] = LoggingType.ALL; - } - bool found = false; - foreach(LoggingType l in _ActiveLoggingTypes) - { - if (l == lt || l == LoggingType.ALL) - { - found = true; - break; - } - } - - - return found; - } - - private static void PrintToConsole(LoggingType lt, string message) - { - Console.WriteLine("[" + lt.ToString() + "]" + message); - } - } -} diff --git a/SMTPServer/SMTPServer/MTACommands.cs b/SMTPServer/SMTPServer/MTACommands.cs deleted file mode 100644 index 30b1588..0000000 --- a/SMTPServer/SMTPServer/MTACommands.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; - -namespace SMTPServer -{ - public class MTACommandsDict : Dictionary - { - public MTACommandsDict() - { - Add(MTACommands.HELO, "HELO"); - Add(MTACommands.DATA, "DATA"); - Add(MTACommands.EXPN, "EXPN"); - Add(MTACommands.HELP, "HELP"); - Add(MTACommands.MAIL_FROM, "MAIL FROM"); - Add(MTACommands.NOOP, "NOOP"); - Add(MTACommands.QUIT, "QUIT"); - Add(MTACommands.RCPT_TO, "RCPT TO"); - Add(MTACommands.RSET, "RSET"); - Add(MTACommands.SAML_FROM, "SAML FROM"); - Add(MTACommands.SEND_FROM, "SEND FROM"); - Add(MTACommands.SOML_FROM, "SOML FROM"); - Add(MTACommands.TURN, "TURN"); - Add(MTACommands.VERB, "VERB"); - Add(MTACommands.VRFY, "VRFY"); - } - } - - public enum MTACommands - { - HELO, - MAIL_FROM, - RCPT_TO, - DATA, - RSET, - QUIT, - HELP, - VRFY, - EXPN, - VERB, - NOOP, - TURN, - SEND_FROM, - SOML_FROM, - SAML_FROM - } -} \ No newline at end of file diff --git a/SMTPServer/SMTPServer/Mail.cs b/SMTPServer/SMTPServer/Mail.cs deleted file mode 100644 index 9ea534e..0000000 --- a/SMTPServer/SMTPServer/Mail.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace SMTPServer -{ - public class Mail - { - public string _To { get; private set; } - public string _From { get; private set; } - - public string _Subject { get; private set; } - public string _MessageId { get; private set; } - public string _Date { get; private set; } - - public string _MIME_Version { get; private set; } - - public string _Others { get; private set; } - - - public Mail(string mail) - { - - } - } -} diff --git a/SMTPServer/SMTPServer/MailQueue.cs b/SMTPServer/SMTPServer/MailQueue.cs deleted file mode 100644 index ab77596..0000000 --- a/SMTPServer/SMTPServer/MailQueue.cs +++ /dev/null @@ -1,65 +0,0 @@ -using SMTPServer.Exceptions; -using System; -using System.Collections.Generic; -using System.Text; - -namespace SMTPServer -{ - public class MailQueue - { - private static List _Mails = null; - - public static void AddToMesssageQueue(Mail mail) - { - lock (_Mails) - { - if (_Mails == null) - { - _Mails = new List(); - } - _Mails.Add(new QueueMail(mail)); - } - } - - public static QueueMail GetNextMail() - { - lock (_Mails) - { - if(_Mails.Count < 1) - { - throw new NoMailsInQueueException(); - } - - var m = _Mails[0]; - - _Mails.Remove(m); - m._Count++; - _Mails.Add(m); - - return m; - } - } - - public static void RemoveMailFromQueue(QueueMail mail) - { - lock (_Mails) - { - _Mails.Remove(mail); - } - } - - public class QueueMail - { - public DateTime _QueueEntered { get; private set; } - public int _Count { get; set; } - public Mail _Mail { get; private set; } - - public QueueMail (Mail mail) - { - _Mail = mail; - _QueueEntered = DateTime.Now; - _Count = 0; - } - } - } -} diff --git a/SMTPServer/SMTPServer/MailTransferAgent.cs b/SMTPServer/SMTPServer/MailTransferAgent.cs deleted file mode 100644 index cdf38f4..0000000 --- a/SMTPServer/SMTPServer/MailTransferAgent.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; -using SMTPServer.Exceptions; -using System.Net; - -namespace SMTPServer -{ - class MailTransferAgent - { - static MTACommandsDict _MTACommandsDict = new MTACommandsDict(); - static Thread _Thread; - - public static void StartMailTransferAgent() - { - _Thread = new Thread(new ThreadStart(MTAAsync)); - _Thread.Start(); - } - - public static async void MTAAsync() - { - while (true) - { - try - { - var mail = MailQueue.GetNextMail(); - } - catch (NoMailsInQueueException) - { - Thread.Sleep(100); - continue; - } - - var charset = Encoding.UTF8; - - var dnsname = await GetDNSNameAsync(""); - var client = new StartTcpConnection(25, new IPAddress(GetIpFromDNS(dnsname)), charset); - - if (!client.Connected) ; //ToDo Errorfall - } - } - - public static async System.Threading.Tasks.Task GetDNSNameAsync(string mailTo) - { - var parts = mailTo.Split('@'); - var domain = parts[1]; - - var dns = await System.Net.Dns.GetHostEntryAsync(domain); - //dns. - - return null; - } - - public static byte[] GetIpFromDNS(string dnsname) - { - - return null; - } - } -} diff --git a/SMTPServer/SMTPServer/PortListener.cs b/SMTPServer/SMTPServer/PortListener.cs deleted file mode 100644 index e5621fe..0000000 --- a/SMTPServer/SMTPServer/PortListener.cs +++ /dev/null @@ -1,65 +0,0 @@ -using SMTPServer.Exceptions; -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; - -namespace SMTPServer -{ - public delegate void ConnectedEventHandler(object source, ConnectedEventArgs e); - - public class ConnectedEventArgs : EventArgs - { - private string EventInfo; - - public Socket Client { get; private set; } - - public ConnectedEventArgs(Socket client) - { - Client = client; - } - } - - public class PortListener - { - static List _Ports = null; - TcpListener _Listener; - - public event ConnectedEventHandler OnConnected; - - public PortListener(int port) - { - if(_Ports == null) - { - _Ports = new List(); - } - - foreach(var i in _Ports) - { - if (i == port) - { - throw new PortUsedException(); - } - } - - _Ports.Add(port); - StartListening(new TcpListener(IPAddress.Any, port)); - } - - private void StartListening(TcpListener listener) - { - listener.Start(); - _Listener = listener; - Thread thread = new Thread(new ThreadStart(ListenerAsync)); - thread.Start(); - } - - private async void ListenerAsync() - { - var client = await _Listener.AcceptSocketAsync(); - OnConnected(this, new ConnectedEventArgs(client)); - } - } -} diff --git a/SMTPServer/SMTPServer/Program.cs b/SMTPServer/SMTPServer/Program.cs deleted file mode 100644 index 85aea45..0000000 --- a/SMTPServer/SMTPServer/Program.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace SMTPServer -{ - class Program - { - static void Main(string[] args) - { - Console.WriteLine("####################"); - - var listener = new PortListener(5122); - listener.OnConnected += Listener_OnConnected; - } - - private static void Listener_OnConnected(object source, ConnectedEventArgs e) - { - throw new NotImplementedException(); - } - } -} \ No newline at end of file diff --git a/SMTPServer/SMTPServer/TCPListener.cs b/SMTPServer/SMTPServer/TCPListener.cs deleted file mode 100644 index a1017ea..0000000 --- a/SMTPServer/SMTPServer/TCPListener.cs +++ /dev/null @@ -1,64 +0,0 @@ -using SMTPServer.Exceptions; -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; - -namespace SMTPServer -{ - public delegate void ConnectedEventHandler(object source, ConnectedEventArgs e); - - public class ConnectedEventArgs : EventArgs - { - private string EventInfo; - - public Socket Client { get; private set; } - - public ConnectedEventArgs(Socket client) - { - Client = client; - } - } - - public class TCPListener - { - static List _Ports = null; - TcpListener _Listener; - - public event ConnectedEventHandler OnConnected; - - public TCPListener(int port) - { - if(_Ports == null) - { - _Ports = new List(); - } - - foreach(var i in _Ports) - { - if (i == port) - { - throw new PortUsedException(); - } - } - - _Ports.Add(port); - StartListening(new TcpListener(IPAddress.Any, port)); - } - - private void StartListening(TcpListener listener) - { - listener.Start(); - _Listener = listener; - Thread thread = new Thread(new ThreadStart(ListenerAsync)); - } - - private async void ListenerAsync() - { - var client = await _Listener.AcceptSocketAsync(); - OnConnected(this, new ConnectedEventArgs(client)); - } - } -}