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 @@ public HostConfigModule(IConfigFileProvider configFileProvider) 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 @@ public HostConfigModule(IConfigFileProvider configFileProvider) 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 @@ private void SaveHostConfig(HostConfigResource resource) _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 @@ public static IRuleBuilderOptions ValidPort(this IRuleBuilder 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 @@ public class UrlAclAdapter : IUrlAclAdapter 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 @@ public UrlAclAdapter(INetshProvider netshProvider, _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 @@ public void ConfigureUrl() 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 @@ public void ConfigureUrl() 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) { - var arguments = String.Format("http show urlacl {0}", urlAcl); - var output = _netshProvider.Run(arguments); - - if (output == null || !output.Standard.Any()) return false; - - return output.Standard.Any(line => line.Contains(urlAcl)); + return RegisteredUrls.Any(c => c.Scheme == urlAcl.Scheme && + c.Address == urlAcl.Address && + c.Port == urlAcl.Port && + c.UrlBase == urlAcl.UrlBase); } - private void RegisterUrl(string urlAcl) + private List GetRegisteredUrls() { - var arguments = String.Format("http add urlacl {0} sddl=D:(A;;GX;;;S-1-1-0)", urlAcl); + var arguments = String.Format("http show urlacl"); + var output = _netshProvider.Run(arguments); + + 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; + + }).Where(r => r != null).ToList(); + } + + private void RegisterUrl(UrlAcl 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 List BuildUrls(string protocol, string url, int port) + private void UnregisterUrl(UrlAcl urlAcl) { - var urls = new List(); + _logger.Trace("Removing URL ACL {0}", urlAcl.Url); + + var arguments = String.Format("http delete urlacl {0}", urlAcl.Url); + _netshProvider.Run(arguments); + } + + private List BuildUrlAcls(string scheme, string address, int port) + { + 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 @@ public void StartServer() } } - _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 @@
+