diff --git a/src/NzbDrone.Common.Test/Http/HttpUriFixture.cs b/src/NzbDrone.Common.Test/Http/HttpUriFixture.cs index 3627b04dc..d25d07bfe 100644 --- a/src/NzbDrone.Common.Test/Http/HttpUriFixture.cs +++ b/src/NzbDrone.Common.Test/Http/HttpUriFixture.cs @@ -1,4 +1,4 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Common.Http; using NzbDrone.Test.Common; @@ -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 258025213..2bdb5406c 100644 --- a/src/NzbDrone.Common/Extensions/StringExtensions.cs +++ b/src/NzbDrone.Common/Extensions/StringExtensions.cs @@ -237,5 +237,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 248e4fddc..647da04ee 100644 --- a/src/NzbDrone.Common/Http/HttpUri.cs +++ b/src/NzbDrone.Common/Http/HttpUri.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; @@ -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/Notifications/MediaBrowser/MediaBrowserProxy.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs index 7162b9a13..ed190cae7 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs @@ -1,4 +1,4 @@ -using NLog; +using NLog; using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs index de30d248b..4f6a6ad9d 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserSettings.cs @@ -1,5 +1,6 @@ -using FluentValidation; +using FluentValidation; using Newtonsoft.Json; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -43,7 +44,7 @@ namespace NzbDrone.Core.Notifications.Emby public bool UpdateLibrary { get; set; } [JsonIgnore] - public string Address => $"{Host}:{Port}"; + public string Address => $"{Host.ToUrlHost()}:{Port}"; public bool IsValid => !string.IsNullOrWhiteSpace(Host) && Port > 0; diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerProxy.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerProxy.cs index 784726e3c..523706334 100644 --- a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerProxy.cs +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerProxy.cs @@ -152,7 +152,7 @@ namespace NzbDrone.Core.Notifications.Plex.Server { var scheme = settings.UseSsl ? "https" : "http"; - var requestBuilder = new HttpRequestBuilder($"{scheme}://{settings.Host}:{settings.Port}") + var requestBuilder = new HttpRequestBuilder($"{scheme}://{settings.Host.ToUrlHost()}:{settings.Port}") .Accept(HttpAccept.Json) .AddQueryParam("X-Plex-Client-Identifier", _configService.PlexClientIdentifier) .AddQueryParam("X-Plex-Product", BuildInfo.AppName) diff --git a/src/NzbDrone.Core/Notifications/Xbmc/XbmcSettings.cs b/src/NzbDrone.Core/Notifications/Xbmc/XbmcSettings.cs index 4c88f66e8..9fd4010cc 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/XbmcSettings.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/XbmcSettings.cs @@ -1,6 +1,7 @@ -using System.ComponentModel; +using System.ComponentModel; using FluentValidation; using Newtonsoft.Json; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -58,7 +59,7 @@ namespace NzbDrone.Core.Notifications.Xbmc public bool AlwaysUpdate { get; set; } [JsonIgnore] - public string Address => string.Format("{0}:{1}", Host, Port); + public string Address => $"{Host.ToUrlHost()}:{Port}"; public NzbDroneValidationResult Validate() { diff --git a/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs b/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs index 8699d156c..9f8fdd0de 100644 --- a/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs +++ b/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs @@ -1,12 +1,15 @@ -using System.Text.RegularExpressions; +using System; +using System.Text.RegularExpressions; using FluentValidation; using FluentValidation.Validators; -using NzbDrone.Core.Parser; +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)); @@ -25,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 = "/sonarr")