From 8a4d309d57e864086ec853aa457451ce746bfad5 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 5 Dec 2022 00:57:21 -0800 Subject: [PATCH] New: IPv6 support for connections/indexers/download clients Closes #2026 (cherry picked from commit 1b90fbcf7df2c1086da4791c6491771924b1b7aa) --- src/NzbDrone.Common.Test/Http/HttpUriFixture.cs | 1 + src/NzbDrone.Common/Extensions/StringExtensions.cs | 5 +++++ src/NzbDrone.Common/Http/HttpUri.cs | 6 ++++-- src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs | 10 ++++++++-- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Common.Test/Http/HttpUriFixture.cs b/src/NzbDrone.Common.Test/Http/HttpUriFixture.cs index f47c59881..d25d07bfe 100644 --- a/src/NzbDrone.Common.Test/Http/HttpUriFixture.cs +++ b/src/NzbDrone.Common.Test/Http/HttpUriFixture.cs @@ -10,6 +10,7 @@ namespace NzbDrone.Common.Test.Http [TestCase("abc://my_host.com:8080/root/api/")] [TestCase("abc://my_host.com:8080//root/api/")] [TestCase("abc://my_host.com:8080/root//api/")] + [TestCase("abc://[::1]:8080/root//api/")] public void should_parse(string uri) { var newUri = new HttpUri(uri); diff --git a/src/NzbDrone.Common/Extensions/StringExtensions.cs b/src/NzbDrone.Common/Extensions/StringExtensions.cs index 21729f6f8..6b6f04b8e 100644 --- a/src/NzbDrone.Common/Extensions/StringExtensions.cs +++ b/src/NzbDrone.Common/Extensions/StringExtensions.cs @@ -398,5 +398,10 @@ namespace NzbDrone.Common.Extensions return parsedAddress.AddressFamily == AddressFamily.InterNetwork || parsedAddress.AddressFamily == AddressFamily.InterNetworkV6; } + + public static string ToUrlHost(this string input) + { + return input.Contains(":") ? $"[{input}]" : input; + } } } diff --git a/src/NzbDrone.Common/Http/HttpUri.cs b/src/NzbDrone.Common/Http/HttpUri.cs index f90e564f5..647da04ee 100644 --- a/src/NzbDrone.Common/Http/HttpUri.cs +++ b/src/NzbDrone.Common/Http/HttpUri.cs @@ -8,7 +8,7 @@ namespace NzbDrone.Common.Http { public class HttpUri : IEquatable { - private static readonly Regex RegexUri = new Regex(@"^(?:(?[a-z]+):)?(?://(?[-_A-Z0-9.]+)(?::(?[0-9]{1,5}))?)?(?(?:(?:(?<=^)|/+)[^/?#\r\n]+)+/*|/+)?(?:\?(?[^#\r\n]*))?(?:\#(?.*))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex RegexUri = new Regex(@"^(?:(?[a-z]+):)?(?://(?[-_A-Z0-9.]+|\[[[A-F0-9:]+\])(?::(?[0-9]{1,5}))?)?(?(?:(?:(?<=^)|/+)[^/?#\r\n]+)+/*|/+)?(?:\?(?[^#\r\n]*))?(?:\#(?.*))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); private readonly string _uri; public string FullUri => _uri; @@ -70,6 +70,8 @@ namespace NzbDrone.Common.Http private void Parse() { + var parseSuccess = Uri.TryCreate(_uri, UriKind.RelativeOrAbsolute, out var uri); + var match = RegexUri.Match(_uri); var scheme = match.Groups["scheme"]; @@ -79,7 +81,7 @@ namespace NzbDrone.Common.Http var query = match.Groups["query"]; var fragment = match.Groups["fragment"]; - if (!match.Success || (scheme.Success && !host.Success && path.Success)) + if (!parseSuccess || (scheme.Success && !host.Success && path.Success)) { throw new ArgumentException("Uri didn't match expected pattern: " + _uri); } diff --git a/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs b/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs index b5e6f3be9..aa416ac1d 100644 --- a/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs +++ b/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs @@ -1,11 +1,15 @@ +using System; using System.Text.RegularExpressions; using FluentValidation; using FluentValidation.Validators; +using NzbDrone.Common.Extensions; namespace NzbDrone.Core.Validation { public static class RuleBuilderExtensions { + private static readonly Regex HostRegex = new Regex("^[-_a-z0-9.]+$", RegexOptions.IgnoreCase | RegexOptions.Compiled); + public static IRuleBuilderOptions ValidId(this IRuleBuilder ruleBuilder) { return ruleBuilder.SetValidator(new GreaterThanValidator(0)); @@ -24,13 +28,15 @@ namespace NzbDrone.Core.Validation public static IRuleBuilderOptions ValidHost(this IRuleBuilder ruleBuilder) { ruleBuilder.SetValidator(new NotEmptyValidator(null)); - return ruleBuilder.SetValidator(new RegularExpressionValidator("^[-_a-z0-9.]+$", RegexOptions.IgnoreCase)).WithMessage("must be valid Host without http://"); + + return ruleBuilder.Must(x => HostRegex.IsMatch(x) || x.IsValidIpAddress()).WithMessage("must be valid Host without http://"); } public static IRuleBuilderOptions ValidRootUrl(this IRuleBuilder ruleBuilder) { ruleBuilder.SetValidator(new NotEmptyValidator(null)); - return ruleBuilder.SetValidator(new RegularExpressionValidator("^https?://[-_a-z0-9.]+", RegexOptions.IgnoreCase)).WithMessage("must be valid URL that starts with http(s)://"); + + return ruleBuilder.Must(x => x.IsValidUrl() && x.StartsWith("http", StringComparison.InvariantCultureIgnoreCase)).WithMessage("must be valid URL that starts with http(s)://"); } public static IRuleBuilderOptions ValidUrlBase(this IRuleBuilder ruleBuilder, string example = "/readarr")