Merge pull request #145 from Sonarr/bind-to-address

Bind to address
This commit is contained in:
Mark McDowall 2014-12-09 23:40:20 -08:00
commit 3a716a5585
10 changed files with 226 additions and 43 deletions

View File

@ -23,9 +23,9 @@ namespace NzbDrone.Api.Config
GetResourceById = GetHostConfig; GetResourceById = GetHostConfig;
UpdateResource = SaveHostConfig; 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.Port).ValidPort();
SharedValidator.RuleFor(c => c.Username).NotEmpty().When(c => c.AuthenticationEnabled); SharedValidator.RuleFor(c => c.Username).NotEmpty().When(c => c.AuthenticationEnabled);
SharedValidator.RuleFor(c => c.Password).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.SslCertHash).NotEmpty().When(c => c.EnableSsl && OsInfo.IsWindows);
SharedValidator.RuleFor(c => c.UpdateScriptPath).IsValidPath().When(c => c.UpdateMechanism == UpdateMechanism.Script); 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() private HostConfigResource GetHostConfig()
@ -58,4 +63,4 @@ namespace NzbDrone.Api.Config
_configFileProvider.SaveConfigDictionary(dictionary); _configFileProvider.SaveConfigDictionary(dictionary);
} }
} }
} }

View File

@ -6,6 +6,7 @@ namespace NzbDrone.Api.Config
{ {
public class HostConfigResource : RestResource public class HostConfigResource : RestResource
{ {
public String BindAddress { get; set; }
public Int32 Port { get; set; } public Int32 Port { get; set; }
public Int32 SslPort { get; set; } public Int32 SslPort { get; set; }
public Boolean EnableSsl { get; set; } public Boolean EnableSsl { get; set; }

View File

@ -23,6 +23,7 @@ namespace NzbDrone.Core.Configuration
Dictionary<string, object> GetConfigDictionary(); Dictionary<string, object> GetConfigDictionary();
void SaveConfigDictionary(Dictionary<string, object> configValues); void SaveConfigDictionary(Dictionary<string, object> configValues);
string BindAddress { get; }
int Port { get; } int Port { get; }
int SslPort { get; } int SslPort { get; }
bool EnableSsl { get; } bool EnableSsl { get; }
@ -110,6 +111,22 @@ namespace NzbDrone.Core.Configuration
_eventAggregator.PublishEvent(new ConfigFileSavedEvent()); _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 public int Port
{ {
get { return GetValueInt("Port", 8989); } get { return GetValueInt("Port", 8989); }

View File

@ -841,6 +841,7 @@
<Compile Include="Update\UpdateVerification.cs" /> <Compile Include="Update\UpdateVerification.cs" />
<Compile Include="Update\UpdateVerificationFailedException.cs" /> <Compile Include="Update\UpdateVerificationFailedException.cs" />
<Compile Include="Validation\FolderValidator.cs" /> <Compile Include="Validation\FolderValidator.cs" />
<Compile Include="Validation\IpValidation.cs" />
<Compile Include="Validation\LangaugeValidator.cs" /> <Compile Include="Validation\LangaugeValidator.cs" />
<Compile Include="Validation\NzbDroneValidationFailure.cs" /> <Compile Include="Validation\NzbDroneValidationFailure.cs" />
<Compile Include="Validation\Paths\DroneFactoryValidator.cs" /> <Compile Include="Validation\Paths\DroneFactoryValidator.cs" />

View File

@ -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<T, string> ValidIp4Address<T>(this IRuleBuilder<T, string> 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<T, string> NotListenAllIp4Address<T>(this IRuleBuilder<T, string> ruleBuilder)
{
return ruleBuilder.SetValidator(new RegularExpressionValidator(@"^(?!0\.0\.0\.0)")).WithMessage("Use * instead of 0.0.0.0");
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -1,15 +1,18 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions;
using NLog; using NLog;
using NzbDrone.Common;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
namespace NzbDrone.Host.AccessControl namespace NzbDrone.Host.AccessControl
{ {
public interface IUrlAclAdapter public interface IUrlAclAdapter
{ {
void ConfigureUrl(); void ConfigureUrls();
List<String> Urls { get; } List<String> Urls { get; }
} }
@ -20,7 +23,18 @@ namespace NzbDrone.Host.AccessControl
private readonly IRuntimeInfo _runtimeInfo; private readonly IRuntimeInfo _runtimeInfo;
private readonly Logger _logger; private readonly Logger _logger;
public List<String> Urls { get; private set; } public List<String> Urls
{
get
{
return InternalUrls.Select(c => c.Url).ToList();
}
}
private List<UrlAcl> InternalUrls { get; set; }
private List<UrlAcl> RegisteredUrls { get; set; }
private static readonly Regex UrlAclRegex = new Regex(@"(?<scheme>https?)\:\/\/(?<address>.+?)\:(?<port>\d+)/(?<urlbase>.+)?", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public UrlAclAdapter(INetshProvider netshProvider, public UrlAclAdapter(INetshProvider netshProvider,
IConfigFileProvider configFileProvider, IConfigFileProvider configFileProvider,
@ -32,35 +46,56 @@ namespace NzbDrone.Host.AccessControl
_runtimeInfo = runtimeInfo; _runtimeInfo = runtimeInfo;
_logger = logger; _logger = logger;
Urls = new List<String>(); InternalUrls = new List<UrlAcl>();
RegisteredUrls = GetRegisteredUrls();
} }
public void ConfigureUrl() public void ConfigureUrls()
{ {
var localHttpUrls = BuildUrls("http", "localhost", _configFileProvider.Port); var localHostHttpUrls = BuildUrlAcls("http", "localhost", _configFileProvider.Port);
var wildcardHttpUrls = BuildUrls("http", "*", _configFileProvider.Port); var interfaceHttpUrls = BuildUrlAcls("http", _configFileProvider.BindAddress, _configFileProvider.Port);
var localHttpsUrls = BuildUrls("https", "localhost", _configFileProvider.SslPort); var localHostHttpsUrls = BuildUrlAcls("https", "localhost", _configFileProvider.SslPort);
var wildcardHttpsUrls = BuildUrls("https", "*", _configFileProvider.SslPort); var interfaceHttpsUrls = BuildUrlAcls("https", _configFileProvider.BindAddress, _configFileProvider.SslPort);
if (!_configFileProvider.EnableSsl) if (!_configFileProvider.EnableSsl)
{ {
localHttpsUrls.Clear(); localHostHttpsUrls.Clear();
wildcardHttpsUrls.Clear(); interfaceHttpsUrls.Clear();
} }
if (OsInfo.IsWindows && !_runtimeInfo.IsAdmin) if (OsInfo.IsWindows && !_runtimeInfo.IsAdmin)
{ {
var httpUrls = wildcardHttpUrls.All(IsRegistered) ? wildcardHttpUrls : localHttpUrls; var httpUrls = interfaceHttpUrls.All(IsRegistered) ? interfaceHttpUrls : localHostHttpUrls;
var httpsUrls = wildcardHttpsUrls.All(IsRegistered) ? wildcardHttpsUrls : localHttpsUrls; var httpsUrls = interfaceHttpsUrls.All(IsRegistered) ? interfaceHttpsUrls : localHostHttpsUrls;
Urls.AddRange(httpUrls); InternalUrls.AddRange(httpUrls);
Urls.AddRange(httpsUrls); 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 else
{ {
Urls.AddRange(wildcardHttpUrls); InternalUrls.AddRange(interfaceHttpUrls);
Urls.AddRange(wildcardHttpsUrls); 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) if (OsInfo.IsWindows)
{ {
@ -71,49 +106,104 @@ namespace NzbDrone.Host.AccessControl
private void RefreshRegistration() private void RefreshRegistration()
{ {
if (OsInfo.Version.Major < 6) if (OsInfo.Version.Major < 6) return;
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); return RegisteredUrls.Any(c => c.Scheme == urlAcl.Scheme &&
var output = _netshProvider.Run(arguments); c.Address == urlAcl.Address &&
c.Port == urlAcl.Port &&
if (output == null || !output.Standard.Any()) return false; c.UrlBase == urlAcl.UrlBase);
return output.Standard.Any(line => line.Contains(urlAcl));
} }
private void RegisterUrl(string urlAcl) private List<UrlAcl> 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<UrlAcl>();
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); _netshProvider.Run(arguments);
} }
private string BuildUrl(string protocol, string url, int port, string urlBase) private void RemoveSimilar(UrlAcl urlAcl)
{ {
var result = protocol + "://" + url + ":" + port; var similar = RegisteredUrls.Where(c => c.Scheme == urlAcl.Scheme &&
result += String.IsNullOrEmpty(urlBase) ? "/" : urlBase + "/"; 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<String> BuildUrls(string protocol, string url, int port) private void UnregisterUrl(UrlAcl urlAcl)
{ {
var urls = new List<String>(); _logger.Trace("Removing URL ACL {0}", urlAcl.Url);
var arguments = String.Format("http delete urlacl {0}", urlAcl.Url);
_netshProvider.Run(arguments);
}
private List<UrlAcl> BuildUrlAcls(string scheme, string address, int port)
{
var urlAcls = new List<UrlAcl>();
var urlBase = _configFileProvider.UrlBase; 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;
} }
} }
} }

View File

@ -101,6 +101,7 @@
</Compile> </Compile>
<Compile Include="AccessControl\FirewallAdapter.cs" /> <Compile Include="AccessControl\FirewallAdapter.cs" />
<Compile Include="AccessControl\NetshProvider.cs" /> <Compile Include="AccessControl\NetshProvider.cs" />
<Compile Include="AccessControl\UrlAcl.cs" />
<Compile Include="AccessControl\SslAdapter.cs" /> <Compile Include="AccessControl\SslAdapter.cs" />
<Compile Include="AccessControl\UrlAclAdapter.cs" /> <Compile Include="AccessControl\UrlAclAdapter.cs" />
<Compile Include="ApplicationModes.cs" /> <Compile Include="ApplicationModes.cs" />

View File

@ -45,7 +45,7 @@ namespace NzbDrone.Host.Owin
} }
} }
_urlAclAdapter.ConfigureUrl(); _urlAclAdapter.ConfigureUrls();
_logger.Info("Listening on the following URLs:"); _logger.Info("Listening on the following URLs:");
foreach (var url in _urlAclAdapter.Urls) foreach (var url in _urlAclAdapter.Urls)

View File

@ -2,6 +2,19 @@
<fieldset> <fieldset>
<legend>Start-Up</legend> <legend>Start-Up</legend>
<div class="form-group advanced-setting">
<label class="col-sm-3 control-label">Bind Address</label>
<div class="col-sm-1 col-sm-push-4 help-inline">
<i class="icon-nd-form-warning" title="Requires restart to take effect" />
<i class="icon-nd-form-info" title="Valid IP4 address or '*' for all interfaces"/>
</div>
<div class="col-sm-4 col-sm-pull-1">
<input type="text" placeholder="*" name="bindAddress" class="form-control" />
</div>
</div>
<div class="form-group"> <div class="form-group">
<label class="col-sm-3 control-label">Port Number</label> <label class="col-sm-3 control-label">Port Number</label>