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")