From 5a8ce04bd5f3dea33c5cdc418aabdbca63e7fdda Mon Sep 17 00:00:00 2001 From: Mike Date: Wed, 1 Oct 2014 14:34:10 -0400 Subject: [PATCH 1/2] Allow binding to specific interface addresses https://trello.com/c/aWazKGQc/404-allow-binding-to-specified-ip-address --- src/NzbDrone.Api/Config/HostConfigModule.cs | 7 +++++-- src/NzbDrone.Api/Config/HostConfigResource.cs | 1 + .../Configuration/ConfigFileProvider.cs | 17 ++++++++++++++++ .../Validation/RuleBuilderExtensions.cs | 19 +++++++++++++++++- .../AccessControl/UrlAclAdapter.cs | 20 +++++++++---------- .../Settings/General/GeneralViewTemplate.hbs | 12 +++++++++++ 6 files changed, 63 insertions(+), 13 deletions(-) diff --git a/src/NzbDrone.Api/Config/HostConfigModule.cs b/src/NzbDrone.Api/Config/HostConfigModule.cs index c5103222c..d8dce397f 100644 --- a/src/NzbDrone.Api/Config/HostConfigModule.cs +++ b/src/NzbDrone.Api/Config/HostConfigModule.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Reflection; using FluentValidation; using NzbDrone.Common.EnvironmentInfo; @@ -25,7 +26,9 @@ namespace NzbDrone.Api.Config SharedValidator.RuleFor(c => c.Branch).NotEmpty().WithMessage("Branch name is required, 'master' is the default"); SharedValidator.RuleFor(c => c.Port).ValidPort(); - + SharedValidator.RuleFor(c => c.BindAddress).ValidIp4Address().When(c => c.BindAddress != "*"); + SharedValidator.RuleFor(c => c.Port).InclusiveBetween(1, 65535); + SharedValidator.RuleFor(c => c.Username).NotEmpty().When(c => c.AuthenticationEnabled); SharedValidator.RuleFor(c => c.Password).NotEmpty().When(c => c.AuthenticationEnabled); diff --git a/src/NzbDrone.Api/Config/HostConfigResource.cs b/src/NzbDrone.Api/Config/HostConfigResource.cs index f8dde71a7..7cdf741ae 100644 --- a/src/NzbDrone.Api/Config/HostConfigResource.cs +++ b/src/NzbDrone.Api/Config/HostConfigResource.cs @@ -6,6 +6,7 @@ namespace NzbDrone.Api.Config { public class HostConfigResource : RestResource { + public String BindAddress { get; set; } public Int32 Port { get; set; } public Int32 SslPort { get; set; } public Boolean EnableSsl { get; set; } diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index 6a9bcffbb..6120ef7a7 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -23,6 +23,7 @@ namespace NzbDrone.Core.Configuration Dictionary GetConfigDictionary(); void SaveConfigDictionary(Dictionary configValues); + string BindAddress { get; } int Port { get; } int SslPort { get; } bool EnableSsl { get; } @@ -110,6 +111,22 @@ namespace NzbDrone.Core.Configuration _eventAggregator.PublishEvent(new ConfigFileSavedEvent()); } + public string BindAddress + { + get + { + const string defaultValue = "*"; + + string bindAddress = GetValue("BindAddress", defaultValue); + if (string.IsNullOrWhiteSpace(bindAddress)) + { + return defaultValue; + } + + return bindAddress; + } + } + public int Port { get { return GetValueInt("Port", 8989); } diff --git a/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs b/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs index b0fb71377..3751ee2b7 100644 --- a/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs +++ b/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs @@ -1,4 +1,6 @@ -using System.Text.RegularExpressions; +using System.Net; +using System.Net.Sockets; +using System.Text.RegularExpressions; using FluentValidation; using FluentValidation.Validators; using NzbDrone.Core.Parser; @@ -33,6 +35,21 @@ namespace NzbDrone.Core.Validation return ruleBuilder.SetValidator(new InclusiveBetweenValidator(1, 65535)); } + public static IRuleBuilderOptions ValidIp4Address(this IRuleBuilder ruleBuilder) + { + + return ruleBuilder.Must(x => + { + IPAddress parsedAddress; + if (!IPAddress.TryParse(x, out parsedAddress)) + { + return false; + } + + return parsedAddress.AddressFamily == AddressFamily.InterNetwork; + }); + } + public static IRuleBuilderOptions ValidLanguage(this IRuleBuilder ruleBuilder) { return ruleBuilder.SetValidator(new LangaugeValidator()); diff --git a/src/NzbDrone.Host/AccessControl/UrlAclAdapter.cs b/src/NzbDrone.Host/AccessControl/UrlAclAdapter.cs index fd2557e02..3b4bd5437 100644 --- a/src/NzbDrone.Host/AccessControl/UrlAclAdapter.cs +++ b/src/NzbDrone.Host/AccessControl/UrlAclAdapter.cs @@ -37,30 +37,30 @@ namespace NzbDrone.Host.AccessControl public void ConfigureUrl() { - var localHttpUrls = BuildUrls("http", "localhost", _configFileProvider.Port); - var wildcardHttpUrls = BuildUrls("http", "*", _configFileProvider.Port); + var localHostHttpUrls = BuildUrls("http", "localhost", _configFileProvider.Port); + var interfaceHttpUrls = BuildUrls("http", _configFileProvider.BindAddress, _configFileProvider.Port); - var localHttpsUrls = BuildUrls("https", "localhost", _configFileProvider.SslPort); - var wildcardHttpsUrls = BuildUrls("https", "*", _configFileProvider.SslPort); + var localHostHttpsUrls = BuildUrls("https", "localhost", _configFileProvider.SslPort); + var interfaceHttpsUrls = BuildUrls("https", _configFileProvider.BindAddress, _configFileProvider.SslPort); if (!_configFileProvider.EnableSsl) { - localHttpsUrls.Clear(); - wildcardHttpsUrls.Clear(); + Urls.Clear(); + interfaceHttpsUrls.Clear(); } if (OsInfo.IsWindows && !_runtimeInfo.IsAdmin) { - var httpUrls = wildcardHttpUrls.All(IsRegistered) ? wildcardHttpUrls : localHttpUrls; - var httpsUrls = wildcardHttpsUrls.All(IsRegistered) ? wildcardHttpsUrls : localHttpsUrls; + var httpUrls = interfaceHttpUrls.All(IsRegistered) ? interfaceHttpUrls : localHostHttpUrls; + var httpsUrls = interfaceHttpsUrls.All(IsRegistered) ? interfaceHttpsUrls : localHostHttpsUrls; Urls.AddRange(httpUrls); Urls.AddRange(httpsUrls); } else { - Urls.AddRange(wildcardHttpUrls); - Urls.AddRange(wildcardHttpsUrls); + Urls.AddRange(interfaceHttpUrls); + Urls.AddRange(interfaceHttpsUrls); if (OsInfo.IsWindows) { diff --git a/src/UI/Settings/General/GeneralViewTemplate.hbs b/src/UI/Settings/General/GeneralViewTemplate.hbs index 9c4e95a2b..c5ed1cadf 100644 --- a/src/UI/Settings/General/GeneralViewTemplate.hbs +++ b/src/UI/Settings/General/GeneralViewTemplate.hbs @@ -2,6 +2,18 @@
Start-Up +
+ + +
+ +
+ +
+ +
+
+
From d77c685d14f9822d6f8f3449c272444d423cd62f Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 23 Nov 2014 23:46:22 -0800 Subject: [PATCH 2/2] Remove existing URL ACLs to avoid conflicts New: Choose IP address to listen on (advanced) Fixed: Check if URL has been registered with Windows before trying to register it --- src/NzbDrone.Api/Config/HostConfigModule.cs | 14 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + src/NzbDrone.Core/Validation/IpValidation.cs | 35 ++++ .../Validation/RuleBuilderExtensions.cs | 19 +-- src/NzbDrone.Host/AccessControl/UrlAcl.cs | 20 +++ .../AccessControl/UrlAclAdapter.cs | 156 ++++++++++++++---- src/NzbDrone.Host/NzbDrone.Host.csproj | 1 + src/NzbDrone.Host/Owin/OwinHostController.cs | 2 +- .../Settings/General/GeneralViewTemplate.hbs | 1 + 9 files changed, 191 insertions(+), 58 deletions(-) create mode 100644 src/NzbDrone.Core/Validation/IpValidation.cs create mode 100644 src/NzbDrone.Host/AccessControl/UrlAcl.cs diff --git a/src/NzbDrone.Api/Config/HostConfigModule.cs b/src/NzbDrone.Api/Config/HostConfigModule.cs index d8dce397f..ee523087c 100644 --- a/src/NzbDrone.Api/Config/HostConfigModule.cs +++ b/src/NzbDrone.Api/Config/HostConfigModule.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Linq; using System.Reflection; using FluentValidation; using NzbDrone.Common.EnvironmentInfo; @@ -24,10 +23,8 @@ namespace NzbDrone.Api.Config GetResourceById = GetHostConfig; UpdateResource = SaveHostConfig; - SharedValidator.RuleFor(c => c.Branch).NotEmpty().WithMessage("Branch name is required, 'master' is the default"); + SharedValidator.RuleFor(c => c.Branch).NotEmpty().WithMessage("Branch name is required, 'master' is the default"); SharedValidator.RuleFor(c => c.Port).ValidPort(); - SharedValidator.RuleFor(c => c.BindAddress).ValidIp4Address().When(c => c.BindAddress != "*"); - SharedValidator.RuleFor(c => c.Port).InclusiveBetween(1, 65535); SharedValidator.RuleFor(c => c.Username).NotEmpty().When(c => c.AuthenticationEnabled); SharedValidator.RuleFor(c => c.Password).NotEmpty().When(c => c.AuthenticationEnabled); @@ -36,6 +33,11 @@ namespace NzbDrone.Api.Config SharedValidator.RuleFor(c => c.SslCertHash).NotEmpty().When(c => c.EnableSsl && OsInfo.IsWindows); SharedValidator.RuleFor(c => c.UpdateScriptPath).IsValidPath().When(c => c.UpdateMechanism == UpdateMechanism.Script); + + SharedValidator.RuleFor(c => c.BindAddress) + .ValidIp4Address() + .NotListenAllIp4Address() + .When(c => c.BindAddress != "*"); } private HostConfigResource GetHostConfig() @@ -61,4 +63,4 @@ namespace NzbDrone.Api.Config _configFileProvider.SaveConfigDictionary(dictionary); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 0c00d49d3..14ec9ed73 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -841,6 +841,7 @@ + diff --git a/src/NzbDrone.Core/Validation/IpValidation.cs b/src/NzbDrone.Core/Validation/IpValidation.cs new file mode 100644 index 000000000..b0a674b39 --- /dev/null +++ b/src/NzbDrone.Core/Validation/IpValidation.cs @@ -0,0 +1,35 @@ +using System.Net; +using System.Net.Sockets; +using FluentValidation; +using FluentValidation.Validators; + +namespace NzbDrone.Core.Validation +{ + public static class IpValidation + { + public static IRuleBuilderOptions ValidIp4Address(this IRuleBuilder ruleBuilder) + { + return ruleBuilder.Must(x => + { + IPAddress parsedAddress; + + if (!IPAddress.TryParse(x, out parsedAddress)) + { + return false; + } + + if (parsedAddress.Equals(IPAddress.Parse("255.255.255.255"))) + { + return false; + } + + return parsedAddress.AddressFamily == AddressFamily.InterNetwork; + }).WithMessage("Must be a valid IPv4 Address"); + } + + public static IRuleBuilderOptions NotListenAllIp4Address(this IRuleBuilder ruleBuilder) + { + return ruleBuilder.SetValidator(new RegularExpressionValidator(@"^(?!0\.0\.0\.0)")).WithMessage("Use * instead of 0.0.0.0"); + } + } +} diff --git a/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs b/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs index 3751ee2b7..b0fb71377 100644 --- a/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs +++ b/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs @@ -1,6 +1,4 @@ -using System.Net; -using System.Net.Sockets; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using FluentValidation; using FluentValidation.Validators; using NzbDrone.Core.Parser; @@ -35,21 +33,6 @@ namespace NzbDrone.Core.Validation return ruleBuilder.SetValidator(new InclusiveBetweenValidator(1, 65535)); } - public static IRuleBuilderOptions ValidIp4Address(this IRuleBuilder ruleBuilder) - { - - return ruleBuilder.Must(x => - { - IPAddress parsedAddress; - if (!IPAddress.TryParse(x, out parsedAddress)) - { - return false; - } - - return parsedAddress.AddressFamily == AddressFamily.InterNetwork; - }); - } - public static IRuleBuilderOptions ValidLanguage(this IRuleBuilder ruleBuilder) { return ruleBuilder.SetValidator(new LangaugeValidator()); diff --git a/src/NzbDrone.Host/AccessControl/UrlAcl.cs b/src/NzbDrone.Host/AccessControl/UrlAcl.cs new file mode 100644 index 000000000..c53ba857b --- /dev/null +++ b/src/NzbDrone.Host/AccessControl/UrlAcl.cs @@ -0,0 +1,20 @@ +using System; + +namespace NzbDrone.Host.AccessControl +{ + public class UrlAcl + { + public string Scheme { get; set; } + public string Address { get; set; } + public int Port { get; set; } + public string UrlBase { get; set; } + + public string Url + { + get + { + return String.Format("{0}://{1}:{2}/{3}", Scheme, Address, Port, UrlBase); + } + } + } +} diff --git a/src/NzbDrone.Host/AccessControl/UrlAclAdapter.cs b/src/NzbDrone.Host/AccessControl/UrlAclAdapter.cs index 3b4bd5437..34ff4619e 100644 --- a/src/NzbDrone.Host/AccessControl/UrlAclAdapter.cs +++ b/src/NzbDrone.Host/AccessControl/UrlAclAdapter.cs @@ -1,15 +1,18 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using NLog; +using NzbDrone.Common; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; namespace NzbDrone.Host.AccessControl { public interface IUrlAclAdapter { - void ConfigureUrl(); + void ConfigureUrls(); List Urls { get; } } @@ -20,7 +23,18 @@ namespace NzbDrone.Host.AccessControl private readonly IRuntimeInfo _runtimeInfo; private readonly Logger _logger; - public List Urls { get; private set; } + public List Urls + { + get + { + return InternalUrls.Select(c => c.Url).ToList(); + } + } + + private List InternalUrls { get; set; } + private List RegisteredUrls { get; set; } + + private static readonly Regex UrlAclRegex = new Regex(@"(?https?)\:\/\/(?
.+?)\:(?\d+)/(?.+)?", RegexOptions.Compiled | RegexOptions.IgnoreCase); public UrlAclAdapter(INetshProvider netshProvider, IConfigFileProvider configFileProvider, @@ -32,20 +46,21 @@ namespace NzbDrone.Host.AccessControl _runtimeInfo = runtimeInfo; _logger = logger; - Urls = new List(); + InternalUrls = new List(); + RegisteredUrls = GetRegisteredUrls(); } - public void ConfigureUrl() + public void ConfigureUrls() { - var localHostHttpUrls = BuildUrls("http", "localhost", _configFileProvider.Port); - var interfaceHttpUrls = BuildUrls("http", _configFileProvider.BindAddress, _configFileProvider.Port); + var localHostHttpUrls = BuildUrlAcls("http", "localhost", _configFileProvider.Port); + var interfaceHttpUrls = BuildUrlAcls("http", _configFileProvider.BindAddress, _configFileProvider.Port); - var localHostHttpsUrls = BuildUrls("https", "localhost", _configFileProvider.SslPort); - var interfaceHttpsUrls = BuildUrls("https", _configFileProvider.BindAddress, _configFileProvider.SslPort); + var localHostHttpsUrls = BuildUrlAcls("https", "localhost", _configFileProvider.SslPort); + var interfaceHttpsUrls = BuildUrlAcls("https", _configFileProvider.BindAddress, _configFileProvider.SslPort); if (!_configFileProvider.EnableSsl) { - Urls.Clear(); + localHostHttpsUrls.Clear(); interfaceHttpsUrls.Clear(); } @@ -54,13 +69,33 @@ namespace NzbDrone.Host.AccessControl var httpUrls = interfaceHttpUrls.All(IsRegistered) ? interfaceHttpUrls : localHostHttpUrls; var httpsUrls = interfaceHttpsUrls.All(IsRegistered) ? interfaceHttpsUrls : localHostHttpsUrls; - Urls.AddRange(httpUrls); - Urls.AddRange(httpsUrls); + InternalUrls.AddRange(httpUrls); + InternalUrls.AddRange(httpsUrls); + + if (_configFileProvider.BindAddress != "*") + { + if (httpUrls.None(c => c.Address.Equals("localhost"))) + { + InternalUrls.AddRange(localHostHttpUrls); + } + + if (httpsUrls.None(c => c.Address.Equals("localhost"))) + { + InternalUrls.AddRange(localHostHttpsUrls); + } + } } else { - Urls.AddRange(interfaceHttpUrls); - Urls.AddRange(interfaceHttpsUrls); + InternalUrls.AddRange(interfaceHttpUrls); + InternalUrls.AddRange(interfaceHttpsUrls); + + //Register localhost URLs so the IP Address doesn't need to be used from the local system + if (_configFileProvider.BindAddress != "*") + { + InternalUrls.AddRange(localHostHttpUrls); + InternalUrls.AddRange(localHostHttpsUrls); + } if (OsInfo.IsWindows) { @@ -71,49 +106,104 @@ namespace NzbDrone.Host.AccessControl private void RefreshRegistration() { - if (OsInfo.Version.Major < 6) - return; + if (OsInfo.Version.Major < 6) return; - Urls.ForEach(RegisterUrl); + foreach (var urlAcl in InternalUrls) + { + if (IsRegistered(urlAcl) || urlAcl.Address.Equals("localhost")) continue; + + RemoveSimilar(urlAcl); + RegisterUrl(urlAcl); + } } - private bool IsRegistered(string urlAcl) + private bool IsRegistered(UrlAcl urlAcl) + { + return RegisteredUrls.Any(c => c.Scheme == urlAcl.Scheme && + c.Address == urlAcl.Address && + c.Port == urlAcl.Port && + c.UrlBase == urlAcl.UrlBase); + } + + private List GetRegisteredUrls() { - var arguments = String.Format("http show urlacl {0}", urlAcl); + var arguments = String.Format("http show urlacl"); var output = _netshProvider.Run(arguments); - if (output == null || !output.Standard.Any()) return false; + if (output == null || !output.Standard.Any()) return new List(); + + return output.Standard.Select(line => + { + var match = UrlAclRegex.Match(line); + + if (match.Success) + { + return new UrlAcl + { + Scheme = match.Groups["scheme"].Value, + Address = match.Groups["address"].Value, + Port = Convert.ToInt32(match.Groups["port"].Value), + UrlBase = match.Groups["urlbase"].Value.Trim() + }; + } + + return null; - return output.Standard.Any(line => line.Contains(urlAcl)); + }).Where(r => r != null).ToList(); } - private void RegisterUrl(string urlAcl) + private void RegisterUrl(UrlAcl urlAcl) { - var arguments = String.Format("http add urlacl {0} sddl=D:(A;;GX;;;S-1-1-0)", urlAcl); + var arguments = String.Format("http add urlacl {0} sddl=D:(A;;GX;;;S-1-1-0)", urlAcl.Url); _netshProvider.Run(arguments); } - private string BuildUrl(string protocol, string url, int port, string urlBase) + private void RemoveSimilar(UrlAcl urlAcl) { - var result = protocol + "://" + url + ":" + port; - result += String.IsNullOrEmpty(urlBase) ? "/" : urlBase + "/"; + var similar = RegisteredUrls.Where(c => c.Scheme == urlAcl.Scheme && + InternalUrls.None(x => x.Address == c.Address) && + c.Port == urlAcl.Port && + c.UrlBase == urlAcl.UrlBase); - return result; + foreach (var s in similar) + { + UnregisterUrl(s); + } + } + + private void UnregisterUrl(UrlAcl urlAcl) + { + _logger.Trace("Removing URL ACL {0}", urlAcl.Url); + + var arguments = String.Format("http delete urlacl {0}", urlAcl.Url); + _netshProvider.Run(arguments); } - private List BuildUrls(string protocol, string url, int port) + private List BuildUrlAcls(string scheme, string address, int port) { - var urls = new List(); + var urlAcls = new List(); var urlBase = _configFileProvider.UrlBase; - if (!String.IsNullOrEmpty(urlBase)) + if (urlBase.IsNotNullOrWhiteSpace()) { - urls.Add(BuildUrl(protocol, url, port, urlBase)); + urlAcls.Add(new UrlAcl + { + Scheme = scheme, + Address = address, + Port = port, + UrlBase = urlBase.Trim('/') + "/" + }); } - urls.Add(BuildUrl(protocol, url, port, "")); + urlAcls.Add(new UrlAcl + { + Scheme = scheme, + Address = address, + Port = port, + UrlBase = String.Empty + }); - return urls; + return urlAcls; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Host/NzbDrone.Host.csproj b/src/NzbDrone.Host/NzbDrone.Host.csproj index a8074931b..0a0cccb44 100644 --- a/src/NzbDrone.Host/NzbDrone.Host.csproj +++ b/src/NzbDrone.Host/NzbDrone.Host.csproj @@ -101,6 +101,7 @@ + diff --git a/src/NzbDrone.Host/Owin/OwinHostController.cs b/src/NzbDrone.Host/Owin/OwinHostController.cs index 6db1a2477..09efd0b24 100644 --- a/src/NzbDrone.Host/Owin/OwinHostController.cs +++ b/src/NzbDrone.Host/Owin/OwinHostController.cs @@ -45,7 +45,7 @@ namespace NzbDrone.Host.Owin } } - _urlAclAdapter.ConfigureUrl(); + _urlAclAdapter.ConfigureUrls(); _logger.Info("Listening on the following URLs:"); foreach (var url in _urlAclAdapter.Urls) diff --git a/src/UI/Settings/General/GeneralViewTemplate.hbs b/src/UI/Settings/General/GeneralViewTemplate.hbs index c5ed1cadf..99067c1cd 100644 --- a/src/UI/Settings/General/GeneralViewTemplate.hbs +++ b/src/UI/Settings/General/GeneralViewTemplate.hbs @@ -7,6 +7,7 @@
+