diff --git a/src/NzbDrone.Api/Config/HostConfigModule.cs b/src/NzbDrone.Api/Config/HostConfigModule.cs index c5103222c..ee523087c 100644 --- a/src/NzbDrone.Api/Config/HostConfigModule.cs +++ b/src/NzbDrone.Api/Config/HostConfigModule.cs @@ -23,9 +23,9 @@ 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.Username).NotEmpty().When(c => c.AuthenticationEnabled); SharedValidator.RuleFor(c => c.Password).NotEmpty().When(c => c.AuthenticationEnabled); @@ -33,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() @@ -58,4 +63,4 @@ namespace NzbDrone.Api.Config _configFileProvider.SaveConfigDictionary(dictionary); } } -} \ No newline at end of file +} 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/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.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 fd2557e02..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,35 +46,56 @@ namespace NzbDrone.Host.AccessControl _runtimeInfo = runtimeInfo; _logger = logger; - Urls = new List(); + InternalUrls = new List(); + RegisteredUrls = GetRegisteredUrls(); } - public void ConfigureUrl() + public void ConfigureUrls() { - var localHttpUrls = BuildUrls("http", "localhost", _configFileProvider.Port); - var wildcardHttpUrls = BuildUrls("http", "*", _configFileProvider.Port); + var localHostHttpUrls = BuildUrlAcls("http", "localhost", _configFileProvider.Port); + var interfaceHttpUrls = BuildUrlAcls("http", _configFileProvider.BindAddress, _configFileProvider.Port); - var localHttpsUrls = BuildUrls("https", "localhost", _configFileProvider.SslPort); - var wildcardHttpsUrls = BuildUrls("https", "*", _configFileProvider.SslPort); + var localHostHttpsUrls = BuildUrlAcls("https", "localhost", _configFileProvider.SslPort); + var interfaceHttpsUrls = BuildUrlAcls("https", _configFileProvider.BindAddress, _configFileProvider.SslPort); if (!_configFileProvider.EnableSsl) { - localHttpsUrls.Clear(); - wildcardHttpsUrls.Clear(); + localHostHttpsUrls.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); + 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(wildcardHttpUrls); - Urls.AddRange(wildcardHttpsUrls); + 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 9c4e95a2b..99067c1cd 100644 --- a/src/UI/Settings/General/GeneralViewTemplate.hbs +++ b/src/UI/Settings/General/GeneralViewTemplate.hbs @@ -2,6 +2,19 @@
Start-Up +
+ + +
+ + +
+ +
+ +
+
+