diff --git a/NatTypeTester.ViewModels/MainWindowViewModel.cs b/NatTypeTester.ViewModels/MainWindowViewModel.cs index 4ec4e2bd1f..dd0832f2b9 100644 --- a/NatTypeTester.ViewModels/MainWindowViewModel.cs +++ b/NatTypeTester.ViewModels/MainWindowViewModel.cs @@ -2,7 +2,7 @@ using DynamicData.Binding; using NatTypeTester.Models; using ReactiveUI; -using STUN.Utils; +using STUN; using System; using System.Collections.Generic; using System.IO; @@ -62,10 +62,9 @@ public void LoadStunServer() return; } - var stun = new StunServer(); foreach (var line in File.ReadLines(path)) { - if (!string.IsNullOrWhiteSpace(line) && stun.Parse(line)) + if (!string.IsNullOrWhiteSpace(line) && StunServer.TryParse(line, out var stun)) { List.Add(stun.ToString()); } diff --git a/NatTypeTester.ViewModels/RFC3489ViewModel.cs b/NatTypeTester.ViewModels/RFC3489ViewModel.cs index b73a48c85d..04bbe5446e 100644 --- a/NatTypeTester.ViewModels/RFC3489ViewModel.cs +++ b/NatTypeTester.ViewModels/RFC3489ViewModel.cs @@ -1,11 +1,12 @@ using Dns.Net.Abstractions; using JetBrains.Annotations; +using Microsoft; using NatTypeTester.Models; using ReactiveUI; +using STUN; using STUN.Client; using STUN.Proxy; using STUN.StunResult; -using STUN.Utils; using System; using System.Net; using System.Reactive; @@ -37,11 +38,7 @@ public RFC3489ViewModel() private async Task TestClassicNatTypeImpl(CancellationToken token) { - var server = new StunServer(); - if (!server.Parse(Config.StunServer)) - { - throw new Exception(@"Wrong STUN Server!"); - } + Verify.Operation(StunServer.TryParse(Config.StunServer, out var server), @"Wrong STUN Server!"); using var proxy = ProxyFactory.CreateProxy( Config.ProxyType, diff --git a/NatTypeTester.ViewModels/RFC5780ViewModel.cs b/NatTypeTester.ViewModels/RFC5780ViewModel.cs index f2f3d85e06..9fe578338b 100644 --- a/NatTypeTester.ViewModels/RFC5780ViewModel.cs +++ b/NatTypeTester.ViewModels/RFC5780ViewModel.cs @@ -1,11 +1,12 @@ using Dns.Net.Abstractions; using JetBrains.Annotations; +using Microsoft; using NatTypeTester.Models; using ReactiveUI; +using STUN; using STUN.Client; using STUN.Proxy; using STUN.StunResult; -using STUN.Utils; using System; using System.Net; using System.Reactive; @@ -37,11 +38,7 @@ public RFC5780ViewModel() private async Task DiscoveryNatTypeImpl(CancellationToken token) { - var server = new StunServer(); - if (!server.Parse(Config.StunServer)) - { - throw new Exception(@"Wrong STUN Server!"); - } + Verify.Operation(StunServer.TryParse(Config.StunServer, out var server), @"Wrong STUN Server!"); using var proxy = ProxyFactory.CreateProxy( Config.ProxyType, diff --git a/STUN/StunServer.cs b/STUN/StunServer.cs new file mode 100644 index 0000000000..da9f058e98 --- /dev/null +++ b/STUN/StunServer.cs @@ -0,0 +1,94 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Sockets; + +namespace STUN +{ + public class StunServer + { + public string Hostname { get; } + public ushort Port { get; } + + private const ushort DefaultPort = 3478; + + public StunServer() + { + Hostname = @"stun.syncthing.net"; + Port = DefaultPort; + } + + private StunServer(string host, ushort port) + { + Hostname = host; + Port = port; + } + + public static bool TryParse(string str, [NotNullWhen(true)] out StunServer? server) + { + server = null; + if (string.IsNullOrEmpty(str)) + { + return false; + } + + var hostLength = str.Length; + var pos = str.LastIndexOf(':'); + + if (pos > 0) + { + if (str[pos - 1] is ']') + { + hostLength = pos; + } + else if (str.AsSpan(0, pos).LastIndexOf(':') is -1) + { + hostLength = pos; + } + } + + var host = str[..hostLength]; + var type = Uri.CheckHostName(host); + switch (type) + { + case UriHostNameType.Dns: + case UriHostNameType.IPv4: + case UriHostNameType.IPv6: + { + break; + } + default: + { + return false; + } + } + + if (hostLength == str.Length) + { + server = new StunServer(host, DefaultPort); + return true; + } + + if (ushort.TryParse(str.AsSpan(hostLength + 1), out var port)) + { + server = new StunServer(host, port); + return true; + } + + return false; + } + + public override string ToString() + { + if (Port is DefaultPort) + { + return Hostname; + } + if (IPAddress.TryParse(Hostname, out var ip) && ip.AddressFamily is AddressFamily.InterNetworkV6) + { + return $@"[{ip}]:{Port}"; + } + return $@"{Hostname}:{Port}"; + } + } +} diff --git a/STUN/Utils/StunServer.cs b/STUN/Utils/StunServer.cs deleted file mode 100644 index 9ec6b3e916..0000000000 --- a/STUN/Utils/StunServer.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using System.Linq; -using System.Net; -using System.Net.Sockets; - -namespace STUN.Utils -{ - public class StunServer - { - public string Hostname { get; set; } - public ushort Port { get; set; } - - public StunServer() - { - Hostname = @"stun.syncthing.net"; - Port = 3478; - } - - public bool Parse(string str) - { - var ipPort = str.Trim().Split(':', ':'); - switch (ipPort.Length) - { - case 0: - return false; - case 1: - { - var host = ipPort[0].Trim(); - if (Uri.CheckHostName(host) != UriHostNameType.Dns && !IPAddress.TryParse(host, out _)) - { - return false; - } - Hostname = host; - Port = 3478; - return true; - } - case 2: - { - var host = ipPort[0].Trim(); - if (Uri.CheckHostName(host) != UriHostNameType.Dns && !IPAddress.TryParse(host, out _)) - { - return false; - } - if (ushort.TryParse(ipPort[1], out var port)) - { - Hostname = host; - Port = port; - return true; - } - break; - } - default: - { - if (IPAddress.TryParse(str.Trim(), out var ipv6)) - { - Hostname = $@"{ipv6}"; - Port = ushort.TryParse(ipPort.Last(), out var portV6) ? portV6 : (ushort)3478; - return true; - } - - var ipStr = string.Join(@":", ipPort, 0, ipPort.Length - 1); - if (!ipStr.StartsWith(@"[") || !ipStr.EndsWith(@"]") || !IPAddress.TryParse(ipStr, out _)) - { - return false; - } - - if (ushort.TryParse(ipPort.Last(), out var port)) - { - Port = port; - return true; - } - - break; - } - } - - return false; - } - - public override string ToString() - { - if (string.IsNullOrEmpty(Hostname)) - { - return string.Empty; - } - if (Port == 3478) - { - return Hostname; - } - if (IPAddress.TryParse(Hostname, out var ip) && ip.AddressFamily != AddressFamily.InterNetwork) - { - return $@"[{Hostname}]:{Port}"; - } - return $@"{Hostname}:{Port}"; - } - } -} diff --git a/UnitTest/StunServerTest.cs b/UnitTest/StunServerTest.cs new file mode 100644 index 0000000000..1b151f61b5 --- /dev/null +++ b/UnitTest/StunServerTest.cs @@ -0,0 +1,70 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using STUN; + +namespace UnitTest +{ + [TestClass] + public class StunServerTest + { + [TestMethod] + [DataRow(@"www.google.com", ushort.MinValue)] + [DataRow(@"1.1.1.1", (ushort)1)] + [DataRow(@"[2001:db8:1234:5678:11:2233:4455:6677]", (ushort)1919)] + public void IsTrue(string host, ushort port) + { + var str = $@"{host}:{port}"; + Assert.IsTrue(StunServer.TryParse(str, out var server)); + Assert.IsNotNull(server); + Assert.AreEqual(host, server.Hostname); + Assert.AreEqual(port, server.Port); + Assert.AreEqual(str, server.ToString()); + } + + [TestMethod] + [DataRow(@"")] + [DataRow(@"www.google.com:114514")] + [DataRow(@"/dw.[/[:114")] + [DataRow(@"2001:db8:1234:5678:11:2233:4455:6677:65535")] + public void IsFalse(string str) + { + Assert.IsFalse(StunServer.TryParse(str, out var server)); + Assert.IsNull(server); + } + + [TestMethod] + [DataRow(@"www.google.com")] + [DataRow(@"1.1.1.1")] + [DataRow(@"2001:db8:1234:5678:11:2233:4455:6677")] + [DataRow(@"[2001:db8:1234:5678:11:2233:4455:6677]")] + [DataRow(@"2001:db8:1234:5678:11:2233:4455:db8")] + public void TestDefaultPort(string str) + { + Assert.IsTrue(StunServer.TryParse(str, out var server)); + Assert.IsNotNull(server); + Assert.AreEqual(str, server.Hostname); + Assert.AreEqual(3478, server.Port); + } + + [TestMethod] + [DataRow(@"stun.syncthing.net:114", @"stun.syncthing.net:114")] + [DataRow(@"stun.syncthing.net:3478", @"stun.syncthing.net")] + [DataRow(@"[2001:db8:1234:5678:11:2233:4455:6677]", @"[2001:db8:1234:5678:11:2233:4455:6677]")] + [DataRow(@"[2001:db8:1234:5678:11:2233:4455:6677]:3478", @"[2001:db8:1234:5678:11:2233:4455:6677]")] + [DataRow(@"1.1.1.1:3478", @"1.1.1.1")] + [DataRow(@"1.1.1.1:1919", @"1.1.1.1:1919")] + public void ToString(string str, string expected) + { + Assert.IsTrue(StunServer.TryParse(str, out var server)); + Assert.IsNotNull(server); + Assert.AreEqual(expected, server.ToString()); + } + + [TestMethod] + public void DefaultServer() + { + var server = new StunServer(); + Assert.AreEqual(@"stun.syncthing.net", server.Hostname); + Assert.AreEqual(3478, server.Port); + } + } +} diff --git a/UnitTest/UnitTest.cs b/UnitTest/UnitTest.cs index e2809e50fc..f6bc4e1ea6 100644 --- a/UnitTest/UnitTest.cs +++ b/UnitTest/UnitTest.cs @@ -2,10 +2,8 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using STUN.Client; using STUN.Enums; -using STUN.Messages.StunAttributeValues; using STUN.Proxy; using System; -using System.Linq; using System.Net; using System.Threading.Tasks; @@ -14,66 +12,6 @@ namespace UnitTest [TestClass] public class UnitTest { - private static ReadOnlySpan MagicCookieAndTransactionId => new byte[] - { - 0x21, 0x12, 0xa4, 0x42, - 0xb7, 0xe7, 0xa7, 0x01, - 0xbc, 0x34, 0xd6, 0x86, - 0xfa, 0x87, 0xdf, 0xae - }; - - private static readonly byte[] XorPort = { 0xa1, 0x47 }; - private static readonly byte[] XorIPv4 = { 0xe1, 0x12, 0xa6, 0x43 }; - private static readonly byte[] XorIPv6 = - { - 0x01, 0x13, 0xa9, 0xfa, - 0xa5, 0xd3, 0xf1, 0x79, - 0xbc, 0x25, 0xf4, 0xb5, - 0xbe, 0xd2, 0xb9, 0xd9 - }; - - private const ushort Port = 32853; - private readonly IPAddress IPv4 = IPAddress.Parse(@"192.0.2.1"); - private readonly IPAddress IPv6 = IPAddress.Parse(@"2001:db8:1234:5678:11:2233:4455:6677"); - - private readonly byte[] _ipv4Response = new byte[] { 0x00, (byte)IpFamily.IPv4 }.Concat(XorPort).Concat(XorIPv4).ToArray(); - private readonly byte[] _ipv6Response = new byte[] { 0x00, (byte)IpFamily.IPv6 }.Concat(XorPort).Concat(XorIPv6).ToArray(); - - /// - /// https://tools.ietf.org/html/rfc5769.html - /// - [TestMethod] - public void TestXorMapped() - { - var t = new XorMappedAddressStunAttributeValue(MagicCookieAndTransactionId) - { - Port = Port, - Family = IpFamily.IPv4, - Address = IPv4 - }; - Span temp = stackalloc byte[ushort.MaxValue]; - - var length4 = t.WriteTo(temp); - Assert.AreNotEqual(0, length4); - Assert.IsTrue(temp[..length4].SequenceEqual(_ipv4Response)); - - t = new XorMappedAddressStunAttributeValue(MagicCookieAndTransactionId); - Assert.IsTrue(t.TryParse(_ipv4Response)); - Assert.AreEqual(t.Port, Port); - Assert.AreEqual(t.Family, IpFamily.IPv4); - Assert.AreEqual(t.Address, IPv4); - - t = new XorMappedAddressStunAttributeValue(MagicCookieAndTransactionId); - Assert.IsTrue(t.TryParse(_ipv6Response)); - Assert.AreEqual(t.Port, Port); - Assert.AreEqual(t.Family, IpFamily.IPv6); - Assert.AreEqual(t.Address, IPv6); - - var length6 = t.WriteTo(temp); - Assert.AreNotEqual(0, length6); - Assert.IsTrue(temp[..length6].SequenceEqual(_ipv6Response)); - } - [TestMethod] public async Task BindingTest() { diff --git a/UnitTest/XorMappedTest.cs b/UnitTest/XorMappedTest.cs new file mode 100644 index 0000000000..da785a33de --- /dev/null +++ b/UnitTest/XorMappedTest.cs @@ -0,0 +1,73 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using STUN.Enums; +using STUN.Messages.StunAttributeValues; +using System; +using System.Linq; +using System.Net; + +namespace UnitTest +{ + [TestClass] + public class XorMappedTest + { + private static ReadOnlySpan MagicCookieAndTransactionId => new byte[] + { + 0x21, 0x12, 0xa4, 0x42, + 0xb7, 0xe7, 0xa7, 0x01, + 0xbc, 0x34, 0xd6, 0x86, + 0xfa, 0x87, 0xdf, 0xae + }; + + private static readonly byte[] XorPort = { 0xa1, 0x47 }; + private static readonly byte[] XorIPv4 = { 0xe1, 0x12, 0xa6, 0x43 }; + private static readonly byte[] XorIPv6 = + { + 0x01, 0x13, 0xa9, 0xfa, + 0xa5, 0xd3, 0xf1, 0x79, + 0xbc, 0x25, 0xf4, 0xb5, + 0xbe, 0xd2, 0xb9, 0xd9 + }; + + private const ushort Port = 32853; + private readonly IPAddress IPv4 = IPAddress.Parse(@"192.0.2.1"); + private readonly IPAddress IPv6 = IPAddress.Parse(@"2001:db8:1234:5678:11:2233:4455:6677"); + + private readonly byte[] _ipv4Response = new byte[] { 0x00, (byte)IpFamily.IPv4 }.Concat(XorPort).Concat(XorIPv4).ToArray(); + private readonly byte[] _ipv6Response = new byte[] { 0x00, (byte)IpFamily.IPv6 }.Concat(XorPort).Concat(XorIPv6).ToArray(); + + /// + /// https://tools.ietf.org/html/rfc5769.html + /// + [TestMethod] + public void TestXorMapped() + { + var t = new XorMappedAddressStunAttributeValue(MagicCookieAndTransactionId) + { + Port = Port, + Family = IpFamily.IPv4, + Address = IPv4 + }; + Span temp = stackalloc byte[ushort.MaxValue]; + + var length4 = t.WriteTo(temp); + Assert.AreNotEqual(0, length4); + Assert.IsTrue(temp[..length4].SequenceEqual(_ipv4Response)); + + t = new XorMappedAddressStunAttributeValue(MagicCookieAndTransactionId); + Assert.IsTrue(t.TryParse(_ipv4Response)); + Assert.AreEqual(t.Port, Port); + Assert.AreEqual(t.Family, IpFamily.IPv4); + Assert.AreEqual(t.Address, IPv4); + + t = new XorMappedAddressStunAttributeValue(MagicCookieAndTransactionId); + Assert.IsTrue(t.TryParse(_ipv6Response)); + Assert.AreEqual(t.Port, Port); + Assert.AreEqual(t.Family, IpFamily.IPv6); + Assert.AreEqual(t.Address, IPv6); + + var length6 = t.WriteTo(temp); + Assert.AreNotEqual(0, length6); + Assert.IsTrue(temp[..length6].SequenceEqual(_ipv6Response)); + } + } +}