diff --git a/frontend/build/webpack.config.js b/frontend/build/webpack.config.js index 11140ad16..1c86e39d4 100644 --- a/frontend/build/webpack.config.js +++ b/frontend/build/webpack.config.js @@ -46,6 +46,14 @@ module.exports = (env) => { alias: { jquery: 'jquery/src/jquery', 'react-middle-truncate': 'react-middle-truncate/lib/react-middle-truncate' + }, + fallback: { + buffer: false, + http: false, + https: false, + url: false, + util: false, + net: false } }, diff --git a/frontend/src/Components/SignalRConnector.js b/frontend/src/Components/SignalRConnector.js index f34bba53b..7d842fa1f 100644 --- a/frontend/src/Components/SignalRConnector.js +++ b/frontend/src/Components/SignalRConnector.js @@ -1,5 +1,4 @@ -import $ from 'jquery'; -import 'signalr'; +import * as signalR from '@microsoft/signalr/dist/browser/signalr.js'; import PropTypes from 'prop-types'; import { Component } from 'react'; import { connect } from 'react-redux'; @@ -16,29 +15,6 @@ import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; import { fetchTags, fetchTagDetails } from 'Store/Actions/tagActions'; import { fetchQualityDefinitions } from 'Store/Actions/settingsActions'; -function getState(status) { - switch (status) { - case 0: - return 'connecting'; - case 1: - return 'connected'; - case 2: - return 'reconnecting'; - case 4: - return 'disconnected'; - default: - throw new Error(`invalid status ${status}`); - } -} - -function isAppDisconnected(disconnectedTime) { - if (!disconnectedTime) { - return false; - } - - return Math.floor(new Date().getTime() / 1000) - disconnectedTime > 180; -} - function getHandlerName(name) { name = titleCase(name); name = name.replace('/', ''); @@ -80,6 +56,37 @@ const mapDispatchToProps = { dispatchFetchTagDetails: fetchTagDetails }; +function Logger(minimumLogLevel) { + this.minimumLogLevel = minimumLogLevel; +} + +Logger.prototype.cleanse = function(message) { + const apikey = new RegExp(`access_token=${window.Sonarr.apiKey}`, 'g'); + return message.replace(apikey, 'access_token=(removed)'); +}; + +Logger.prototype.log = function(logLevel, message) { + // see https://github.com/aspnet/AspNetCore/blob/21c9e2cc954c10719878839cd3f766aca5f57b34/src/SignalR/clients/ts/signalr/src/Utils.ts#L147 + if (logLevel >= this.minimumLogLevel) { + switch (logLevel) { + case signalR.LogLevel.Critical: + case signalR.LogLevel.Error: + console.error(`[signalR] ${signalR.LogLevel[logLevel]}: ${this.cleanse(message)}`); + break; + case signalR.LogLevel.Warning: + console.warn(`[signalR] ${signalR.LogLevel[logLevel]}: ${this.cleanse(message)}`); + break; + case signalR.LogLevel.Information: + console.info(`[signalR] ${signalR.LogLevel[logLevel]}: ${this.cleanse(message)}`); + break; + default: + // console.debug only goes to attached debuggers in Node, so we use console.log for Trace and Debug + console.log(`[signalR] ${signalR.LogLevel[logLevel]}: ${this.cleanse(message)}`); + break; + } + } +}; + class SignalRConnector extends Component { // @@ -88,58 +95,43 @@ class SignalRConnector extends Component { constructor(props, context) { super(props, context); - this.signalRconnectionOptions = { transport: ['webSockets', 'serverSentEvents', 'longPolling'] }; - this.signalRconnection = null; - this.retryInterval = 1; - this.retryTimeoutId = null; - this.disconnectedTime = null; + this.connection = null; } componentDidMount() { - console.log('Starting signalR'); + console.log('[signalR] starting'); - const url = `${window.Sonarr.urlBase}/signalr`; + const url = `${window.Sonarr.urlBase}/signalr/messages`; - this.signalRconnection = $.connection(url, { apiKey: window.Sonarr.apiKey }); + this.connection = new signalR.HubConnectionBuilder() + .configureLogging(new Logger(signalR.LogLevel.Information)) + .withUrl(`${url}?access_token=${window.Sonarr.apiKey}`) + .withAutomaticReconnect({ + nextRetryDelayInMilliseconds: (retryContext) => { + if (retryContext.elapsedMilliseconds > 180000) { + this.props.dispatchSetAppValue({ isDisconnected: true }); + } + return Math.min(retryContext.previousRetryCount, 10) * 1000; + } + }) + .build(); - this.signalRconnection.stateChanged(this.onStateChanged); - this.signalRconnection.received(this.onReceived); - this.signalRconnection.reconnecting(this.onReconnecting); - this.signalRconnection.disconnected(this.onDisconnected); + this.connection.onreconnecting(this.onReconnecting); + this.connection.onreconnected(this.onReconnected); + this.connection.onclose(this.onClose); - this.signalRconnection.start(this.signalRconnectionOptions); + this.connection.on('receiveMessage', this.onReceiveMessage); + + this.connection.start().then(this.onStart, this.onStartFail); } componentWillUnmount() { - if (this.retryTimeoutId) { - this.retryTimeoutId = clearTimeout(this.retryTimeoutId); - } - - this.signalRconnection.stop(); - this.signalRconnection = null; + this.connection.stop(); + this.connection = null; } // // Control - - retryConnection = () => { - if (isAppDisconnected(this.disconnectedTime)) { - this.setState({ - isDisconnected: true - }); - } - - this.retryTimeoutId = setTimeout(() => { - if (!this.signalRconnection) { - console.error('signalR: Connection was disposed'); - return; - } - - this.signalRconnection.start(this.signalRconnectionOptions); - this.retryInterval = Math.min(this.retryInterval + 1, 10); - }, this.retryInterval * 1000); - } - handleMessage = (message) => { const { name, @@ -242,7 +234,7 @@ class SignalRConnector extends Component { } handleVersion = (body) => { - const version = body.Version; + const version = body.version; this.props.dispatchSetVersion({ version }); } @@ -286,80 +278,63 @@ class SignalRConnector extends Component { // // Listeners - onStateChanged = (change) => { - const state = getState(change.newState); - console.log(`signalR: ${state}`); - - if (state === 'connected') { - // Clear disconnected time - this.disconnectedTime = null; - - const { - dispatchFetchCommands, - dispatchFetchSeries, - dispatchSetAppValue - } = this.props; - - // Repopulate the page (if a repopulator is set) to ensure things - // are in sync after reconnecting. - - if (this.props.isReconnecting || this.props.isDisconnected) { - dispatchFetchSeries(); - dispatchFetchCommands(); - repopulatePage(); - } - - dispatchSetAppValue({ - isConnected: true, - isReconnecting: false, - isDisconnected: false, - isRestarting: false - }); - - this.retryInterval = 5; - - if (this.retryTimeoutId) { - clearTimeout(this.retryTimeoutId); - } - } - } - - onReceived = (message) => { - console.debug('signalR: received', message.name, message.body); - - this.handleMessage(message); - } - - onReconnecting = () => { - if (window.Sonarr.unloading) { - return; - } - - if (!this.disconnectedTime) { - this.disconnectedTime = Math.floor(new Date().getTime() / 1000); - } - - this.props.dispatchSetAppValue({ - isReconnecting: true - }); - } - - onDisconnected = () => { - if (window.Sonarr.unloading) { - return; - } - - if (!this.disconnectedTime) { - this.disconnectedTime = Math.floor(new Date().getTime() / 1000); - } + onStartFail = (error) => { + console.error('[signalR] failed to connect'); + console.error(error); this.props.dispatchSetAppValue({ isConnected: false, - isReconnecting: true, - isDisconnected: isAppDisconnected(this.disconnectedTime) + isReconnecting: false, + isDisconnected: false, + isRestarting: false + }); + } + + onStart = () => { + console.debug('[signalR] connected'); + + this.props.dispatchSetAppValue({ + isConnected: true, + isReconnecting: false, + isDisconnected: false, + isRestarting: false + }); + } + + onReconnecting = () => { + this.props.dispatchSetAppValue({ isReconnecting: true }); + } + + onReconnected = () => { + + const { + dispatchFetchCommands, + dispatchFetchSeries, + dispatchSetAppValue + } = this.props; + + dispatchSetAppValue({ + isConnected: true, + isReconnecting: false, + isDisconnected: false, + isRestarting: false }); - this.retryConnection(); + // Repopulate the page (if a repopulator is set) to ensure things + // are in sync after reconnecting. + dispatchFetchSeries(); + dispatchFetchCommands(); + repopulatePage(); + } + + onClose = () => { + console.debug('[signalR] connection closed'); + } + + onReceiveMessage = (message) => { + console.debug('[signalR] received', message.name, message.body); + + this.handleMessage(message); } // diff --git a/frontend/src/Settings/General/HostSettings.js b/frontend/src/Settings/General/HostSettings.js index 7133d926e..4c382f413 100644 --- a/frontend/src/Settings/General/HostSettings.js +++ b/frontend/src/Settings/General/HostSettings.js @@ -22,7 +22,8 @@ function HostSettings(props) { instanceName, enableSsl, sslPort, - sslCertHash, + sslCertPath, + sslCertPassword, launchBrowser } = settings; @@ -126,19 +127,40 @@ function HostSettings(props) { } { - isWindows && enableSsl.value ? + enableSsl.value ? - SSL Cert Hash + SSL Cert Path + : + null + } + + { + enableSsl.value ? + + SSL Cert Password + + : null diff --git a/package.json b/package.json index 66647263a..b91be8dca 100644 --- a/package.json +++ b/package.json @@ -29,12 +29,12 @@ "@fortawesome/free-regular-svg-icons": "5.15.3", "@fortawesome/free-solid-svg-icons": "5.15.3", "@fortawesome/react-fontawesome": "0.1.14", + "@microsoft/signalr": "5.0.5", "@sentry/browser": "6.3.1", "@sentry/integrations": "6.3.1", "classnames": "2.3.1", "clipboard": "2.0.8", - "connected-react-router": "6.8.0", - "create-react-class": "15.7.0", + "connected-react-router": "6.9.1", "element-class": "0.2.2", "filesize": "6.3.0", "fuse.js": "6.4.6", @@ -77,8 +77,7 @@ "redux-batched-actions": "0.5.0", "redux-localstorage": "0.4.1", "redux-thunk": "2.3.0", - "reselect": "4.0.0", - "signalr": "2.4.2" + "reselect": "4.0.0" }, "devDependencies": { "@babel/core": "7.13.16", diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index b2d8ca51a..3a523df9f 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -36,7 +36,8 @@ namespace NzbDrone.Core.Configuration string ConsoleLogLevel { get; } string Branch { get; } string ApiKey { get; } - string SslCertHash { get; } + string SslCertPath { get; } + string SslCertPassword { get; } string UrlBase { get; } string UiFolder { get; } string InstanceName { get; } @@ -102,15 +103,12 @@ namespace NzbDrone.Core.Configuration continue; } - if (configValue.Key.Equals("SslCertHash", StringComparison.InvariantCultureIgnoreCase) && configValue.Value.ToString().IsNotNullOrWhiteSpace()) - { - SetValue(configValue.Key.FirstCharToUpper(), HiddenCharacterRegex.Replace(configValue.Value.ToString(), string.Empty)); - continue; - } - object currentValue; allWithDefaults.TryGetValue(configValue.Key, out currentValue); - if (currentValue == null) continue; + if (currentValue == null) + { + continue; + } var equal = configValue.Value.ToString().Equals(currentValue.ToString()); @@ -186,7 +184,8 @@ namespace NzbDrone.Core.Configuration public string LogLevel => GetValue("LogLevel", "info"); public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false); - public string SslCertHash => GetValue("SslCertHash", ""); + public string SslCertPath => GetValue("SslCertPath", ""); + public string SslCertPassword => GetValue("SslCertPassword", ""); public string UrlBase { diff --git a/src/NzbDrone.Core/Sonarr.Core.csproj b/src/NzbDrone.Core/Sonarr.Core.csproj index c40cf391f..9bb78c817 100644 --- a/src/NzbDrone.Core/Sonarr.Core.csproj +++ b/src/NzbDrone.Core/Sonarr.Core.csproj @@ -6,6 +6,7 @@ + diff --git a/src/NzbDrone.Core/Validation/Paths/FileExistsValidator.cs b/src/NzbDrone.Core/Validation/Paths/FileExistsValidator.cs new file mode 100644 index 000000000..9adb200aa --- /dev/null +++ b/src/NzbDrone.Core/Validation/Paths/FileExistsValidator.cs @@ -0,0 +1,26 @@ +using FluentValidation.Validators; +using NzbDrone.Common.Disk; + +namespace NzbDrone.Core.Validation.Paths +{ + public class FileExistsValidator : PropertyValidator + { + private readonly IDiskProvider _diskProvider; + + public FileExistsValidator(IDiskProvider diskProvider) + : base("File does not exist") + { + _diskProvider = diskProvider; + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) + { + return false; + } + + return _diskProvider.FileExists(context.PropertyValue.ToString()); + } + } +} diff --git a/src/NzbDrone.Host.Test/ContainerFixture.cs b/src/NzbDrone.Host.Test/ContainerFixture.cs index 32f95f263..594897892 100644 --- a/src/NzbDrone.Host.Test/ContainerFixture.cs +++ b/src/NzbDrone.Host.Test/ContainerFixture.cs @@ -1,20 +1,22 @@ using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Moq; using NUnit.Framework; using NzbDrone.Common; +using NzbDrone.Common.Composition; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Download; +using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Indexers; using NzbDrone.Core.Jobs; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; +using NzbDrone.SignalR; using NzbDrone.Host; using NzbDrone.Test.Common; -using FluentAssertions; -using System.Linq; -using NzbDrone.Common.Composition; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Download.TrackedDownloads; namespace NzbDrone.App.Test { @@ -31,6 +33,10 @@ namespace NzbDrone.App.Test _container = MainAppContainerBuilder.BuildContainer(args); _container.Register(new MainDatabase(null)); + + // set up a dummy broadcaster to allow tests to resolve + var mockBroadcaster = new Mock(); + _container.Register(mockBroadcaster.Object); } [Test] diff --git a/src/NzbDrone.Host.Test/RouterTest.cs b/src/NzbDrone.Host.Test/RouterTest.cs index 018ab3c21..843061504 100644 --- a/src/NzbDrone.Host.Test/RouterTest.cs +++ b/src/NzbDrone.Host.Test/RouterTest.cs @@ -58,7 +58,7 @@ namespace NzbDrone.App.Test Subject.Route(ApplicationModes.Interactive); - Mocker.GetMock().Verify(c => c.Start(), Times.Once()); + Mocker.GetMock().Verify(c => c.Start(), Times.Once()); } [Test] diff --git a/src/NzbDrone.Host/AccessControl/NetshProvider.cs b/src/NzbDrone.Host/AccessControl/NetshProvider.cs deleted file mode 100644 index 88bcd880c..000000000 --- a/src/NzbDrone.Host/AccessControl/NetshProvider.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using NLog; -using NzbDrone.Common.Processes; - -namespace NzbDrone.Host.AccessControl -{ - public interface INetshProvider - { - ProcessOutput Run(string arguments); - } - - public class NetshProvider : INetshProvider - { - private readonly IProcessProvider _processProvider; - private readonly Logger _logger; - - public NetshProvider(IProcessProvider processProvider, Logger logger) - { - _processProvider = processProvider; - _logger = logger; - } - - public ProcessOutput Run(string arguments) - { - try - { - var output = _processProvider.StartAndCapture("netsh.exe", arguments); - - return output; - } - catch (Exception ex) - { - _logger.Warn(ex, "Error executing netsh with arguments: " + arguments); - } - - return null; - } - } -} diff --git a/src/NzbDrone.Host/AccessControl/SslAdapter.cs b/src/NzbDrone.Host/AccessControl/SslAdapter.cs deleted file mode 100644 index 12784ba87..000000000 --- a/src/NzbDrone.Host/AccessControl/SslAdapter.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System.Linq; -using System.Text.RegularExpressions; -using NLog; -using NzbDrone.Core.Configuration; - -namespace NzbDrone.Host.AccessControl -{ - public interface ISslAdapter - { - void Register(); - } - - public class SslAdapter : ISslAdapter - { - private const string APP_ID = "C2172AF4-F9A6-4D91-BAEE-C2E4EE680613"; - private static readonly Regex CertificateHashRegex = new Regex(@"^\s+(?:Certificate Hash\s+:\s+)(?\w+)", RegexOptions.Compiled); - - private readonly INetshProvider _netshProvider; - private readonly IConfigFileProvider _configFileProvider; - private readonly Logger _logger; - - public SslAdapter(INetshProvider netshProvider, IConfigFileProvider configFileProvider, Logger logger) - { - _netshProvider = netshProvider; - _configFileProvider = configFileProvider; - _logger = logger; - } - - public void Register() - { - if (!_configFileProvider.EnableSsl) return; - if (IsRegistered()) return; - - if (string.IsNullOrWhiteSpace(_configFileProvider.SslCertHash)) - { - _logger.Warn("Unable to enable SSL, SSL Cert Hash is required"); - return; - } - - var arguments = string.Format("http add sslcert ipport=0.0.0.0:{0} certhash={1} appid={{{2}}}", - _configFileProvider.SslPort, - _configFileProvider.SslCertHash, - APP_ID); - - //TODO: Validate that the cert was added properly, invisible spaces FTL - _netshProvider.Run(arguments); - } - - private bool IsRegistered() - { - var ipPort = "0.0.0.0:" + _configFileProvider.SslPort; - var arguments = string.Format("http show sslcert ipport={0}", ipPort); - - var output = _netshProvider.Run(arguments); - - if (output == null || !output.Standard.Any()) return false; - - var hashLine = output.Standard.SingleOrDefault(line => CertificateHashRegex.IsMatch(line.Content)); - - if (hashLine != null) - { - var match = CertificateHashRegex.Match(hashLine.Content); - - if (match.Success) - { - if (match.Groups["hash"].Value != _configFileProvider.SslCertHash) - { - Unregister(); - - return false; - } - } - } - - return output.Standard.Any(line => line.Content.Contains(ipPort)); - } - - private void Unregister() - { - var ipPort = "0.0.0.0:" + _configFileProvider.SslPort; - var arguments = string.Format("http delete sslcert ipport={0}", ipPort); - - _netshProvider.Run(arguments); - } - } -} diff --git a/src/NzbDrone.Host/AccessControl/UrlAcl.cs b/src/NzbDrone.Host/AccessControl/UrlAcl.cs deleted file mode 100644 index 51af167a6..000000000 --- a/src/NzbDrone.Host/AccessControl/UrlAcl.cs +++ /dev/null @@ -1,12 +0,0 @@ -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 => 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 deleted file mode 100644 index ba32e165d..000000000 --- a/src/NzbDrone.Host/AccessControl/UrlAclAdapter.cs +++ /dev/null @@ -1,237 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using NLog; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Exceptions; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Configuration; - -namespace NzbDrone.Host.AccessControl -{ - public interface IUrlAclAdapter - { - void ConfigureUrls(); - List Urls { get; } - } - - public class UrlAclAdapter : IUrlAclAdapter - { - private readonly INetshProvider _netshProvider; - private readonly IConfigFileProvider _configFileProvider; - private readonly IRuntimeInfo _runtimeInfo; - private readonly IOsInfo _osInfo; - private readonly Logger _logger; - - public List Urls - { - get - { - return InternalUrls.Select(c => c.Url).ToList(); - } - } - - private List InternalUrls { get; } - private List RegisteredUrls { get; set; } - - private static readonly Regex UrlAclRegex = new Regex(@"(?https?)\:\/\/(?
.+?)\:(?\d+)/(?.+)?", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public UrlAclAdapter(INetshProvider netshProvider, - IConfigFileProvider configFileProvider, - IRuntimeInfo runtimeInfo, - IOsInfo osInfo, - Logger logger) - { - _netshProvider = netshProvider; - _configFileProvider = configFileProvider; - _runtimeInfo = runtimeInfo; - _osInfo = osInfo; - _logger = logger; - - InternalUrls = new List(); - RegisteredUrls = new List(); - } - - public void ConfigureUrls() - { - var enableSsl = _configFileProvider.EnableSsl; - var port = _configFileProvider.Port; - var sslPort = _configFileProvider.SslPort; - - if (enableSsl && sslPort == port) - { - throw new SonarrStartupException("Cannot use the same port for HTTP and HTTPS. Port {0}", port); - } - - if (RegisteredUrls.Empty()) - { - GetRegisteredUrls(); - } - - var localHostHttpUrls = BuildUrlAcls("http", "localhost", port); - var interfaceHttpUrls = BuildUrlAcls("http", _configFileProvider.BindAddress, port); - - var localHostHttpsUrls = BuildUrlAcls("https", "localhost", sslPort); - var interfaceHttpsUrls = BuildUrlAcls("https", _configFileProvider.BindAddress, sslPort); - - if (!enableSsl) - { - localHostHttpsUrls.Clear(); - interfaceHttpsUrls.Clear(); - } - - if (OsInfo.IsWindows && !_runtimeInfo.IsAdmin) - { - var httpUrls = interfaceHttpUrls.All(IsRegistered) ? interfaceHttpUrls : localHostHttpUrls; - var httpsUrls = interfaceHttpsUrls.All(IsRegistered) ? interfaceHttpsUrls : localHostHttpsUrls; - - 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 - { - 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) - { - RefreshRegistration(); - } - } - } - - private void RefreshRegistration() - { - var osVersion = new Version(_osInfo.Version); - if (osVersion.Major < 6) return; - - foreach (var urlAcl in InternalUrls) - { - if (IsRegistered(urlAcl) || urlAcl.Address.Equals("localhost")) continue; - - RemoveSimilar(urlAcl); - RegisterUrl(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 void GetRegisteredUrls() - { - if (OsInfo.IsNotWindows) - { - return; - } - - if (RegisteredUrls.Any()) - { - return; - } - - var arguments = string.Format("http show urlacl"); - var output = _netshProvider.Run(arguments); - - if (output == null || !output.Standard.Any()) return; - - RegisteredUrls = output.Standard.Select(line => - { - var match = UrlAclRegex.Match(line.Content); - - 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 void RemoveSimilar(UrlAcl urlAcl) - { - var similar = RegisteredUrls.Where(c => c.Scheme == urlAcl.Scheme && - InternalUrls.None(x => x.Address == c.Address) && - c.Port == urlAcl.Port && - c.UrlBase == urlAcl.UrlBase); - - 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 BuildUrlAcls(string scheme, string address, int port) - { - var urlAcls = new List(); - var urlBase = _configFileProvider.UrlBase; - - if (urlBase.IsNotNullOrWhiteSpace()) - { - urlAcls.Add(new UrlAcl - { - Scheme = scheme, - Address = address, - Port = port, - UrlBase = urlBase.Trim('/') + "/" - }); - } - - urlAcls.Add(new UrlAcl - { - Scheme = scheme, - Address = address, - Port = port, - UrlBase = string.Empty - }); - - return urlAcls; - } - } -} diff --git a/src/NzbDrone.Host/ApplicationServer.cs b/src/NzbDrone.Host/ApplicationServer.cs index c904a637f..3a39765f3 100644 --- a/src/NzbDrone.Host/ApplicationServer.cs +++ b/src/NzbDrone.Host/ApplicationServer.cs @@ -7,17 +7,54 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.Datastore; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Events; -using NzbDrone.Host.Owin; namespace NzbDrone.Host { public interface INzbDroneServiceFactory { ServiceBase Build(); - void Start(); } - public class NzbDroneServiceFactory : ServiceBase, INzbDroneServiceFactory, IHandle + public interface INzbDroneConsoleFactory + { + void Start(); + void Shutdown(); + } + + public class NzbDroneServiceFactory : ServiceBase, INzbDroneServiceFactory + { + private readonly INzbDroneConsoleFactory _consoleFactory; + + public NzbDroneServiceFactory(INzbDroneConsoleFactory consoleFactory) + { + _consoleFactory = consoleFactory; + } + + protected override void OnStart(string[] args) + { + _consoleFactory.Start(); + } + + protected override void OnStop() + { + _consoleFactory.Shutdown(); + } + + public ServiceBase Build() + { + return this; + } + } + + public class DummyNzbDroneServiceFactory : INzbDroneServiceFactory + { + public ServiceBase Build() + { + return null; + } + } + + public class NzbDroneConsoleFactory : INzbDroneConsoleFactory, IHandle { private readonly IConfigFileProvider _configFileProvider; private readonly IRuntimeInfo _runtimeInfo; @@ -27,7 +64,8 @@ namespace NzbDrone.Host private readonly IContainer _container; private readonly Logger _logger; - public NzbDroneServiceFactory(IConfigFileProvider configFileProvider, + // private CancelHandler _cancelHandler; + public NzbDroneConsoleFactory(IConfigFileProvider configFileProvider, IHostController hostController, IRuntimeInfo runtimeInfo, IStartupContext startupContext, @@ -44,16 +82,12 @@ namespace NzbDrone.Host _logger = logger; } - protected override void OnStart(string[] args) - { - Start(); - } - public void Start() { if (OsInfo.IsNotWindows) { - Console.CancelKeyPress += (sender, eventArgs) => LogManager.Configuration = null; + //Console.CancelKeyPress += (sender, eventArgs) => eventArgs.Cancel = true; + //_cancelHandler = new CancelHandler(); } _runtimeInfo.IsExiting = false; @@ -77,17 +111,7 @@ namespace NzbDrone.Host _container.Resolve().PublishEvent(new ApplicationStartedEvent()); } - protected override void OnStop() - { - Shutdown(); - } - - public ServiceBase Build() - { - return this; - } - - private void Shutdown() + public void Shutdown() { _logger.Info("Attempting to stop application."); _hostController.StopServer(); diff --git a/src/NzbDrone.Host/Owin/IHostController.cs b/src/NzbDrone.Host/IHostController.cs similarity index 76% rename from src/NzbDrone.Host/Owin/IHostController.cs rename to src/NzbDrone.Host/IHostController.cs index 130b48d4b..858b785ad 100644 --- a/src/NzbDrone.Host/Owin/IHostController.cs +++ b/src/NzbDrone.Host/IHostController.cs @@ -1,8 +1,8 @@ -namespace NzbDrone.Host.Owin +namespace NzbDrone.Host { public interface IHostController { void StartServer(); void StopServer(); } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Host/IRemoteAccessAdapter.cs b/src/NzbDrone.Host/IRemoteAccessAdapter.cs new file mode 100644 index 000000000..7021411a9 --- /dev/null +++ b/src/NzbDrone.Host/IRemoteAccessAdapter.cs @@ -0,0 +1,7 @@ +namespace NzbDrone.Host.AccessControl +{ + public interface IRemoteAccessAdapter + { + void MakeAccessible(bool passive); + } +} diff --git a/src/NzbDrone.Host/MainAppContainerBuilder.cs b/src/NzbDrone.Host/MainAppContainerBuilder.cs index 151677ffa..ffb632519 100644 --- a/src/NzbDrone.Host/MainAppContainerBuilder.cs +++ b/src/NzbDrone.Host/MainAppContainerBuilder.cs @@ -26,9 +26,18 @@ namespace NzbDrone.Host private MainAppContainerBuilder(StartupContext args, List assemblies) : base(args, assemblies) { - AutoRegisterImplementations(); + AutoRegisterImplementations(); Container.Register(); + + if (OsInfo.IsWindows) + { + Container.Register(); + } + else + { + Container.Register(); + } } } } diff --git a/src/NzbDrone.Host/Owin/MiddleWare/IOwinMiddleWare.cs b/src/NzbDrone.Host/Owin/MiddleWare/IOwinMiddleWare.cs deleted file mode 100644 index 1b5e8ce5b..000000000 --- a/src/NzbDrone.Host/Owin/MiddleWare/IOwinMiddleWare.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Owin; - -namespace NzbDrone.Host.Owin.MiddleWare -{ - public interface IOwinMiddleWare - { - int Order { get; } - void Attach(IAppBuilder appBuilder); - } -} \ No newline at end of file diff --git a/src/NzbDrone.Host/Owin/MiddleWare/NzbDroneVersionMiddleWare.cs b/src/NzbDrone.Host/Owin/MiddleWare/NzbDroneVersionMiddleWare.cs deleted file mode 100644 index ca3c79fdb..000000000 --- a/src/NzbDrone.Host/Owin/MiddleWare/NzbDroneVersionMiddleWare.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.Owin; -using NLog; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Instrumentation; -using Owin; - -namespace NzbDrone.Host.Owin.MiddleWare -{ - public class NzbDroneVersionMiddleWare : IOwinMiddleWare - { - public int Order => 0; - - public void Attach(IAppBuilder appBuilder) - { - appBuilder.Use(typeof(AddApplicationVersionHeader)); - } - } - - public class AddApplicationVersionHeader : OwinMiddleware - { - private readonly KeyValuePair _versionHeader; - private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(AddApplicationVersionHeader)); - - public AddApplicationVersionHeader(OwinMiddleware next) - : base(next) - { - _versionHeader = new KeyValuePair("X-Application-Version", new[] { BuildInfo.Version.ToString() }); - } - - public override async Task Invoke(IOwinContext context) - { - try - { - context.Response.Headers.Add(_versionHeader); - await Next.Invoke(context); - } - catch (Exception) - { - Logger.Debug("Unable to set version header"); - } - } - } -} diff --git a/src/NzbDrone.Host/Owin/MiddleWare/SignalRMiddleWare.cs b/src/NzbDrone.Host/Owin/MiddleWare/SignalRMiddleWare.cs deleted file mode 100644 index 5319474d0..000000000 --- a/src/NzbDrone.Host/Owin/MiddleWare/SignalRMiddleWare.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using Microsoft.AspNet.SignalR; -using NzbDrone.Common.Composition; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.SignalR; -using Owin; - -namespace NzbDrone.Host.Owin.MiddleWare -{ - public class SignalRMiddleWare : IOwinMiddleWare - { - public int Order => 1; - - public SignalRMiddleWare(IContainer container) - { - SignalRDependencyResolver.Register(container); - SignalRJsonSerializer.Register(); - - // Note there are some important timeouts involved here: - // nginx has a default 60 sec proxy_read_timeout, this means the connection will be terminated if the server doesn't send anything within that time. - // Previously we lowered the ConnectionTimeout from 110s to 55s to remedy that, however all we should've done is set an appropriate KeepAlive. - // By default KeepAlive is 1/3rd of the DisconnectTimeout, which we set incredibly high 5 years ago, resulting in KeepAlive being 1 minute. - // So when adjusting these values in the future, please keep that all in mind. - GlobalHost.Configuration.ConnectionTimeout = TimeSpan.FromSeconds(110); - GlobalHost.Configuration.DisconnectTimeout = TimeSpan.FromSeconds(180); - GlobalHost.Configuration.KeepAlive = TimeSpan.FromSeconds(30); - } - - public void Attach(IAppBuilder appBuilder) - { - appBuilder.MapSignalR("/signalr", typeof(NzbDronePersistentConnection), new ConnectionConfiguration()); - } - } -} diff --git a/src/NzbDrone.Host/Owin/NlogTextWriter.cs b/src/NzbDrone.Host/Owin/NlogTextWriter.cs deleted file mode 100644 index 2d04acf1a..000000000 --- a/src/NzbDrone.Host/Owin/NlogTextWriter.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.IO; -using System.Text; -using NLog; - -namespace NzbDrone.Host.Owin -{ - public class NlogTextWriter : TextWriter - { - private readonly Logger _logger; - - public NlogTextWriter(Logger logger) - { - _logger = logger; - } - - public override Encoding Encoding => Encoding.Default; - - public override void Write(char[] buffer, int index, int count) - { - Write(buffer); - } - public override void Write(char[] buffer) - { - Write(new string(buffer)); - } - - public override void Write(string value) - { - _logger.Log(GetLogLevel(value), value); - } - - public override void Write(char value) - { - _logger.Trace(value); - } - - private LogLevel GetLogLevel(string value) - { - var lower = value.ToLowerInvariant(); - - if (!lower.Contains("error")) - { - return LogLevel.Trace; - } - - if (lower.Contains("sqlite")) - { - return LogLevel.Trace; - } - - if (lower.Contains("\"errors\":null")) - { - return LogLevel.Trace; - } - - if (lower.Contains("signalr")) - { - if (lower.Contains("an operation was attempted on a nonexistent network connection")) - { - return LogLevel.Trace; - } - - if (lower.Contains("the network connection was aborted by the local system")) - { - return LogLevel.Trace; - } - - if (lower.Contains("the socket has been shut down")) - { - return LogLevel.Trace; - } - } - - return LogLevel.Error; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Host/Owin/OwinHostController.cs b/src/NzbDrone.Host/Owin/OwinHostController.cs deleted file mode 100644 index 3befa0b78..000000000 --- a/src/NzbDrone.Host/Owin/OwinHostController.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using NLog; -using NzbDrone.Host.AccessControl; - -namespace NzbDrone.Host.Owin -{ - public class OwinHostController : IHostController - { - private readonly IOwinAppFactory _owinAppFactory; - private readonly IRemoteAccessAdapter _remoteAccessAdapter; - private readonly IUrlAclAdapter _urlAclAdapter; - private readonly Logger _logger; - private IDisposable _owinApp; - - public OwinHostController( - IOwinAppFactory owinAppFactory, - IRemoteAccessAdapter remoteAccessAdapter, - IUrlAclAdapter urlAclAdapter, - Logger logger) - { - _owinAppFactory = owinAppFactory; - _remoteAccessAdapter = remoteAccessAdapter; - _urlAclAdapter = urlAclAdapter; - _logger = logger; - } - - public void StartServer() - { - _remoteAccessAdapter.MakeAccessible(true); - - _logger.Info("Listening on the following URLs:"); - foreach (var url in _urlAclAdapter.Urls) - { - _logger.Info(" {0}", url); - } - - _owinApp = _owinAppFactory.CreateApp(_urlAclAdapter.Urls); - } - - public void StopServer() - { - if (_owinApp == null) return; - - _logger.Info("Attempting to stop OWIN host"); - _owinApp.Dispose(); - _owinApp = null; - _logger.Info("Host has stopped"); - } - } -} diff --git a/src/NzbDrone.Host/Owin/OwinServiceProvider.cs b/src/NzbDrone.Host/Owin/OwinServiceProvider.cs deleted file mode 100644 index 1bb1993db..000000000 --- a/src/NzbDrone.Host/Owin/OwinServiceProvider.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Reflection; -using Microsoft.Owin.Hosting; -using Microsoft.Owin.Hosting.Engine; -using Microsoft.Owin.Hosting.Services; -using Microsoft.Owin.Hosting.Tracing; -using NLog; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Core.Configuration; -using NzbDrone.Host.Owin.MiddleWare; -using Owin; - -namespace NzbDrone.Host.Owin -{ - public interface IOwinAppFactory - { - IDisposable CreateApp(List urls); - } - - public class OwinAppFactory : IOwinAppFactory - { - private readonly IEnumerable _owinMiddleWares; - private readonly IConfigFileProvider _configFileProvider; - private readonly Logger _logger; - - public OwinAppFactory(IEnumerable owinMiddleWares, IConfigFileProvider configFileProvider, Logger logger) - { - _owinMiddleWares = owinMiddleWares; - _configFileProvider = configFileProvider; - _logger = logger; - } - - public IDisposable CreateApp(List urls) - { - var services = CreateServiceFactory(); - var engine = services.GetService(); - - var options = new StartOptions() - { - ServerFactory = "Microsoft.Owin.Host.HttpListener" - }; - - urls.ForEach(options.Urls.Add); - - var context = new StartContext(options) { Startup = BuildApp }; - - - try - { - return engine.Start(context); - } - catch (TargetInvocationException ex) - { - if (ex.InnerException == null) - { - throw; - } - - if (ex.InnerException is HttpListenerException) - { - throw new PortInUseException("Unable to bind to the designated IP Address/Port ({0}:{1}). Please ensure Sonarr is not already running, the bind address is correct (or is set to'*') and the port is not used", ex, _configFileProvider.BindAddress, _configFileProvider.Port); - } - - throw ex.InnerException; - } - } - - - private void BuildApp(IAppBuilder appBuilder) - { - appBuilder.Properties["host.AppName"] = BuildInfo.AppName; - - foreach (var middleWare in _owinMiddleWares.OrderBy(c => c.Order)) - { - _logger.Debug("Attaching {0} to host", middleWare.GetType().Name); - middleWare.Attach(appBuilder); - } - } - - - private IServiceProvider CreateServiceFactory() - { - var provider = (ServiceProvider)ServicesFactory.Create(); - provider.Add(typeof(ITraceOutputFactory), typeof(OwinTraceOutputFactory)); - - return provider; - } - } -} diff --git a/src/NzbDrone.Host/Owin/OwinTraceOutputFactory.cs b/src/NzbDrone.Host/Owin/OwinTraceOutputFactory.cs deleted file mode 100644 index 6dc0e57ee..000000000 --- a/src/NzbDrone.Host/Owin/OwinTraceOutputFactory.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.IO; -using Microsoft.Owin.Hosting.Tracing; -using NLog; - -namespace NzbDrone.Host.Owin -{ - public class OwinTraceOutputFactory : ITraceOutputFactory - { - private readonly Logger _logger = LogManager.GetLogger("Owin"); - - public TextWriter Create(string outputFile) - { - return new NlogTextWriter(_logger); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Host/Owin/PortInUseException.cs b/src/NzbDrone.Host/Owin/PortInUseException.cs deleted file mode 100644 index 5c6d7a542..000000000 --- a/src/NzbDrone.Host/Owin/PortInUseException.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using NzbDrone.Common.Exceptions; - -namespace NzbDrone.Host.Owin -{ - public class PortInUseException : NzbDroneException - { - public PortInUseException(string message, Exception innerException, params object[] args) : base(message, innerException, args) - { - } - } -} diff --git a/src/NzbDrone.Host/Router.cs b/src/NzbDrone.Host/Router.cs index bb047a514..fd7563617 100644 --- a/src/NzbDrone.Host/Router.cs +++ b/src/NzbDrone.Host/Router.cs @@ -9,6 +9,7 @@ namespace NzbDrone.Host { public class Router { + private readonly INzbDroneConsoleFactory _nzbDroneConsoleFactory; private readonly INzbDroneServiceFactory _nzbDroneServiceFactory; private readonly IServiceProvider _serviceProvider; private readonly IConsoleService _consoleService; @@ -17,7 +18,8 @@ namespace NzbDrone.Host private readonly IRemoteAccessAdapter _remoteAccessAdapter; private readonly Logger _logger; - public Router(INzbDroneServiceFactory nzbDroneServiceFactory, + public Router(INzbDroneConsoleFactory nzbDroneConsoleFactory, + INzbDroneServiceFactory nzbDroneServiceFactory, IServiceProvider serviceProvider, IConsoleService consoleService, IRuntimeInfo runtimeInfo, @@ -25,6 +27,7 @@ namespace NzbDrone.Host IRemoteAccessAdapter remoteAccessAdapter, Logger logger) { + _nzbDroneConsoleFactory = nzbDroneConsoleFactory; _nzbDroneServiceFactory = nzbDroneServiceFactory; _serviceProvider = serviceProvider; _consoleService = consoleService; @@ -48,13 +51,11 @@ namespace NzbDrone.Host break; } - + case ApplicationModes.Interactive: { _logger.Debug(_runtimeInfo.IsWindowsTray ? "Tray selected" : "Console selected"); - - _nzbDroneServiceFactory.Start(); - + _nzbDroneConsoleFactory.Start(); break; } case ApplicationModes.InstallService: diff --git a/src/NzbDrone.Host/Sonarr.Host.csproj b/src/NzbDrone.Host/Sonarr.Host.csproj index e7a03d8ae..a29ca0cbd 100644 --- a/src/NzbDrone.Host/Sonarr.Host.csproj +++ b/src/NzbDrone.Host/Sonarr.Host.csproj @@ -4,8 +4,11 @@ x86 - - + + + + + diff --git a/src/NzbDrone.Host/AccessControl/RemoteAccessAdapter.cs b/src/NzbDrone.Host/WebHost/AccessControl/RemoteAccessAdapter.cs similarity index 65% rename from src/NzbDrone.Host/AccessControl/RemoteAccessAdapter.cs rename to src/NzbDrone.Host/WebHost/AccessControl/RemoteAccessAdapter.cs index de6f68e65..518f2584f 100644 --- a/src/NzbDrone.Host/AccessControl/RemoteAccessAdapter.cs +++ b/src/NzbDrone.Host/WebHost/AccessControl/RemoteAccessAdapter.cs @@ -2,27 +2,16 @@ using NzbDrone.Common.EnvironmentInfo; namespace NzbDrone.Host.AccessControl { - public interface IRemoteAccessAdapter - { - void MakeAccessible(bool passive); - } - public class RemoteAccessAdapter : IRemoteAccessAdapter { private readonly IRuntimeInfo _runtimeInfo; - private readonly IUrlAclAdapter _urlAclAdapter; private readonly IFirewallAdapter _firewallAdapter; - private readonly ISslAdapter _sslAdapter; public RemoteAccessAdapter(IRuntimeInfo runtimeInfo, - IUrlAclAdapter urlAclAdapter, - IFirewallAdapter firewallAdapter, - ISslAdapter sslAdapter) + IFirewallAdapter firewallAdapter) { _runtimeInfo = runtimeInfo; - _urlAclAdapter = urlAclAdapter; _firewallAdapter = firewallAdapter; - _sslAdapter = sslAdapter; } public void MakeAccessible(bool passive) @@ -32,15 +21,12 @@ namespace NzbDrone.Host.AccessControl if (_runtimeInfo.IsAdmin) { _firewallAdapter.MakeAccessible(); - _sslAdapter.Register(); } else if (!passive) { throw new RemoteAccessException("Failed to register URLs for Sonarr. Sonarr will not be accessible remotely"); } } - - _urlAclAdapter.ConfigureUrls(); } } } diff --git a/src/NzbDrone.Host/WebHost/Middleware/IAspNetCoreMiddleware.cs b/src/NzbDrone.Host/WebHost/Middleware/IAspNetCoreMiddleware.cs new file mode 100644 index 000000000..8121fd19b --- /dev/null +++ b/src/NzbDrone.Host/WebHost/Middleware/IAspNetCoreMiddleware.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Builder; + +namespace NzbDrone.Host.Middleware +{ + public interface IAspNetCoreMiddleware + { + int Order { get; } + void Attach(IApplicationBuilder appBuilder); + } +} diff --git a/src/NzbDrone.Host/Owin/MiddleWare/NancyMiddleWare.cs b/src/NzbDrone.Host/WebHost/Middleware/NancyMiddleware.cs similarity index 56% rename from src/NzbDrone.Host/Owin/MiddleWare/NancyMiddleWare.cs rename to src/NzbDrone.Host/WebHost/Middleware/NancyMiddleware.cs index 89f664864..489bea524 100644 --- a/src/NzbDrone.Host/Owin/MiddleWare/NancyMiddleWare.cs +++ b/src/NzbDrone.Host/WebHost/Middleware/NancyMiddleware.cs @@ -1,21 +1,21 @@ -using Nancy.Bootstrapper; +using Microsoft.AspNetCore.Builder; +using Nancy.Bootstrapper; using Nancy.Owin; -using Owin; -namespace NzbDrone.Host.Owin.MiddleWare +namespace NzbDrone.Host.Middleware { - public class NancyMiddleWare : IOwinMiddleWare + public class NancyMiddleware : IAspNetCoreMiddleware { private readonly INancyBootstrapper _nancyBootstrapper; - public NancyMiddleWare(INancyBootstrapper nancyBootstrapper) + public int Order => 2; + + public NancyMiddleware(INancyBootstrapper nancyBootstrapper) { _nancyBootstrapper = nancyBootstrapper; } - public int Order => 2; - - public void Attach(IAppBuilder appBuilder) + public void Attach(IApplicationBuilder appBuilder) { var options = new NancyOptions { @@ -23,7 +23,7 @@ namespace NzbDrone.Host.Owin.MiddleWare PerformPassThrough = context => context.Request.Path.StartsWith("/signalr") }; - appBuilder.UseNancy(options); + appBuilder.UseOwin(x => x.UseNancy(options)); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Host/WebHost/Middleware/SignalRMiddleware.cs b/src/NzbDrone.Host/WebHost/Middleware/SignalRMiddleware.cs new file mode 100644 index 000000000..0250ac3be --- /dev/null +++ b/src/NzbDrone.Host/WebHost/Middleware/SignalRMiddleware.cs @@ -0,0 +1,72 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.DependencyInjection; +using NLog; +using NzbDrone.Common.Composition; +using NzbDrone.Core.Configuration; +using NzbDrone.Host.Middleware; +using NzbDrone.SignalR; + +namespace NzbDrone.Host.Middleware +{ + public class SignalRMiddleware : IAspNetCoreMiddleware + { + private readonly IContainer _container; + private readonly Logger _logger; + private static string API_KEY; + public int Order => 1; + + public SignalRMiddleware(IContainer container, + IConfigFileProvider configFileProvider, + Logger logger) + { + _container = container; + _logger = logger; + API_KEY = configFileProvider.ApiKey; + } + + public void Attach(IApplicationBuilder appBuilder) + { + appBuilder.UseWebSockets(); + + appBuilder.Use(async (context, next) => + { + if (context.Request.Path.StartsWithSegments("/signalr") && + !context.Request.Path.Value.EndsWith("/negotiate")) + { + if (!context.Request.Query.ContainsKey("access_token") || + context.Request.Query["access_token"] != API_KEY) + { + context.Response.StatusCode = 401; + await context.Response.WriteAsync("Unauthorized"); + return; + } + } + + try + { + await next(); + } + catch (OperationCanceledException e) + { + // Demote the exception to trace logging so users don't worry (as much). + _logger.Trace(e); + } + }); + + appBuilder.UseSignalR(routes => + { + routes.MapHub("/signalr/messages"); + }); + + // This is a side effect of haing multiple IoC containers, TinyIoC and whatever + // Kestrel/SignalR is using. Ideally we'd have one IoC container, but that's non-trivial with TinyIoC + // TODO: Use a single IoC container if supported for TinyIoC or if we switch to another system (ie Autofac). + + var hubContext = appBuilder.ApplicationServices.GetService>(); + _container.Register(hubContext); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Host/WebHost/WebHostController.cs b/src/NzbDrone.Host/WebHost/WebHostController.cs new file mode 100644 index 000000000..547a2ba9e --- /dev/null +++ b/src/NzbDrone.Host/WebHost/WebHostController.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NLog; +using NLog.Extensions.Logging; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Configuration; +using NzbDrone.Host; +using NzbDrone.Host.AccessControl; +using NzbDrone.Host.Middleware; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; + +namespace NzbDrone.Host +{ + public class WebHostController : IHostController + { + private readonly IRuntimeInfo _runtimeInfo; + private readonly IConfigFileProvider _configFileProvider; + private readonly IFirewallAdapter _firewallAdapter; + private readonly IEnumerable _middlewares; + private readonly Logger _logger; + private IWebHost _host; + + public WebHostController(IRuntimeInfo runtimeInfo, + IConfigFileProvider configFileProvider, + IFirewallAdapter firewallAdapter, + IEnumerable middlewares, + Logger logger) + { + _runtimeInfo = runtimeInfo; + _configFileProvider = configFileProvider; + _firewallAdapter = firewallAdapter; + _middlewares = middlewares; + _logger = logger; + } + + public void StartServer() + { + if (OsInfo.IsWindows) + { + if (_runtimeInfo.IsAdmin) + { + _firewallAdapter.MakeAccessible(); + } + } + + var bindAddress = _configFileProvider.BindAddress; + var enableSsl = _configFileProvider.EnableSsl; + var sslCertPath = _configFileProvider.SslCertPath; + + var urls = new List(); + + urls.Add(BuildUrl("http", bindAddress, _configFileProvider.Port)); + + if (enableSsl && sslCertPath.IsNotNullOrWhiteSpace()) + { + urls.Add(BuildUrl("https", bindAddress, _configFileProvider.SslPort)); + } + + _host = new WebHostBuilder() + .UseUrls(urls.ToArray()) + .UseKestrel(options => + { + if (enableSsl && sslCertPath.IsNotNullOrWhiteSpace()) + { + options.ConfigureHttpsDefaults(configureOptions => + { + var certificate = new X509Certificate2(); + certificate.Import(_configFileProvider.SslCertPath, _configFileProvider.SslCertPassword, X509KeyStorageFlags.DefaultKeySet); + + configureOptions.ServerCertificate = certificate; + }); + } + }) + .ConfigureKestrel(serverOptions => + { + serverOptions.AllowSynchronousIO = true; + }) + .ConfigureLogging(logging => + { + logging.AddProvider(new NLogLoggerProvider()); + logging.SetMinimumLevel(LogLevel.Warning); + }) + .ConfigureServices(services => + { + services + .AddSignalR() + .AddJsonProtocol(options => + { + options.PayloadSerializerSettings = Json.GetSerializerSettings(); + }); + }) + .Configure(app => + { + app.UsePathBase(_configFileProvider.UrlBase); + app.Properties["host.AppName"] = BuildInfo.AppName; + + foreach (var middleWare in _middlewares.OrderBy(c => c.Order)) + { + _logger.Debug("Attaching {0} to host", middleWare.GetType().Name); + middleWare.Attach(app); + } + }) + .UseContentRoot(Directory.GetCurrentDirectory()) + .Build(); + + _logger.Info("Listening on the following URLs:"); + + foreach (var url in urls) + { + _logger.Info(" {0}", url); + } + + _host.Start(); + } + + public async void StopServer() + { + _logger.Info("Attempting to stop OWIN host"); + + await _host.StopAsync(TimeSpan.FromSeconds(5)); + _host.Dispose(); + _host = null; + + _logger.Info("Host has stopped"); + } + + private string BuildUrl(string scheme, string bindAddress, int port) + { + return $"{scheme}://{bindAddress}:{port}"; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Integration.Test/ApiTests/RootFolderFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/RootFolderFixture.cs index 7376c1a6a..5445073b8 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/RootFolderFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/RootFolderFixture.cs @@ -18,7 +18,7 @@ namespace NzbDrone.Integration.Test.ApiTests [Ignore("SignalR on CI seems unstable")] public void should_add_and_delete_root_folders() { - ConnectSignalR(); + ConnectSignalR().Wait(); var rootFolder = new RootFolderResource { diff --git a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs index 7b82e82e1..cfcb140c8 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs @@ -4,9 +4,9 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; +using System.Threading.Tasks; using FluentAssertions; -using Microsoft.AspNet.SignalR.Client; -using Microsoft.AspNet.SignalR.Client.Transports; +using Microsoft.AspNetCore.SignalR.Client; using NLog; using NLog.Config; using NLog.Targets; @@ -60,7 +60,8 @@ namespace NzbDrone.Integration.Test public ClientBase WantedCutoffUnmet; private List _signalRReceived; - private Connection _signalrConnection; + + private HubConnection _signalrConnection; protected IEnumerable SignalRMessages => _signalRReceived; @@ -143,19 +144,11 @@ namespace NzbDrone.Integration.Test } [TearDown] - public void IntegrationTearDown() + public async Task IntegrationTearDown() { if (_signalrConnection != null) { - switch (_signalrConnection.State) - { - case ConnectionState.Connected: - case ConnectionState.Connecting: - { - _signalrConnection.Stop(); - break; - } - } + await _signalrConnection.StopAsync(); _signalrConnection = null; _signalRReceived = new List(); @@ -182,33 +175,48 @@ namespace NzbDrone.Integration.Test return path; } - protected void ConnectSignalR() + protected async Task ConnectSignalR() { _signalRReceived = new List(); - _signalrConnection = new Connection("http://localhost:8989/signalr"); - _signalrConnection.Start(new LongPollingTransport()).ContinueWith(task => + _signalrConnection = new HubConnectionBuilder().WithUrl("http://localhost:7878/signalr/messages").Build(); + + var cts = new CancellationTokenSource(); + + _signalrConnection.Closed += e => { - if (task.IsFaulted) - { - Assert.Fail("SignalrConnection failed. {0}", task.Exception.GetBaseException()); - } + cts.Cancel(); + return Task.CompletedTask; + }; + + _signalrConnection.On("receiveMessage", (message) => + { + _signalRReceived.Add(message); }); + var connected = false; var retryCount = 0; - while (_signalrConnection.State != ConnectionState.Connected) + while (!connected) { - if (retryCount > 25) + try { - Assert.Fail("Couldn't establish signalr connection. State: {0}", _signalrConnection.State); + Console.WriteLine("Connecting to signalR"); + + await _signalrConnection.StartAsync(); + connected = true; + break; + } + catch (Exception) + { + if (retryCount > 25) + { + Assert.Fail("Couldn't establish signalR connection"); + } } retryCount++; - Console.WriteLine("Connecting to signalR" + _signalrConnection.State); Thread.Sleep(200); } - - _signalrConnection.Received += json => _signalRReceived.Add(Json.Deserialize(json)); ; } public static void WaitForCompletion(Func predicate, int timeout = 10000, int interval = 500) @@ -217,13 +225,17 @@ namespace NzbDrone.Integration.Test for (var i = 0; i < count; i++) { if (predicate()) + { return; + } Thread.Sleep(interval); } if (predicate()) + { return; + } Assert.Fail("Timed on wait"); } diff --git a/src/NzbDrone.Integration.Test/Sonarr.Integration.Test.csproj b/src/NzbDrone.Integration.Test/Sonarr.Integration.Test.csproj index 8e8238b10..a26128225 100644 --- a/src/NzbDrone.Integration.Test/Sonarr.Integration.Test.csproj +++ b/src/NzbDrone.Integration.Test/Sonarr.Integration.Test.csproj @@ -4,7 +4,7 @@ x86 - + diff --git a/src/NzbDrone.SignalR/IBroadcastSignalRMessage.cs b/src/NzbDrone.SignalR/IBroadcastSignalRMessage.cs new file mode 100644 index 000000000..9b16fcf60 --- /dev/null +++ b/src/NzbDrone.SignalR/IBroadcastSignalRMessage.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace NzbDrone.SignalR +{ + public interface IBroadcastSignalRMessage + { + bool IsConnected { get; } + Task BroadcastMessage(SignalRMessage message); + } +} diff --git a/src/NzbDrone.SignalR/MessageHub.cs b/src/NzbDrone.SignalR/MessageHub.cs new file mode 100644 index 000000000..9d0494e34 --- /dev/null +++ b/src/NzbDrone.SignalR/MessageHub.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR; +using NzbDrone.Common.EnvironmentInfo; + +namespace NzbDrone.SignalR +{ + public class SignalRMessageBroadcaster : IBroadcastSignalRMessage + { + private readonly IHubContext _hubContext; + + public SignalRMessageBroadcaster(IHubContext hubContext) + { + _hubContext = hubContext; + } + + public async Task BroadcastMessage(SignalRMessage message) + { + await _hubContext.Clients.All.SendAsync("receiveMessage", message); + } + + public bool IsConnected => MessageHub.IsConnected; + } + + public class MessageHub : Hub + { + private static HashSet _connections = new HashSet(); + + public static bool IsConnected + { + get + { + lock (_connections) + { + return _connections.Count != 0; + } + } + } + + public override async Task OnConnectedAsync() + { + lock (_connections) + { + _connections.Add(Context.ConnectionId); + } + + var message = new SignalRMessage + { + Name = "version", + Body = new + { + Version = BuildInfo.Version.ToString() + } + }; + + await Clients.All.SendAsync("receiveMessage", message); + await base.OnConnectedAsync(); + } + + public override async Task OnDisconnectedAsync(Exception exception) + { + lock (_connections) + { + _connections.Remove(Context.ConnectionId); + } + + await base.OnDisconnectedAsync(exception); + } + } +} diff --git a/src/NzbDrone.SignalR/NoOpPerformanceCounter.cs b/src/NzbDrone.SignalR/NoOpPerformanceCounter.cs deleted file mode 100644 index 3f17b7933..000000000 --- a/src/NzbDrone.SignalR/NoOpPerformanceCounter.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Diagnostics; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace NzbDrone.SignalR -{ - public class NoOpPerformanceCounter : IPerformanceCounter - { - public string CounterName - { - get - { - return GetType().Name; - } - } - - public long Decrement() - { - return 0; - } - - public long Increment() - { - return 0; - } - - public long IncrementBy(long value) - { - return 0; - } - - public long RawValue - { - get { return 0; } - set { } - } - - public void Close() - { - - } - - public void RemoveInstance() - { - - } - - public CounterSample NextSample() - { - return CounterSample.Empty; - } - } -} diff --git a/src/NzbDrone.SignalR/NzbDronePersistentConnection.cs b/src/NzbDrone.SignalR/NzbDronePersistentConnection.cs deleted file mode 100644 index d0a57bb79..000000000 --- a/src/NzbDrone.SignalR/NzbDronePersistentConnection.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNet.SignalR; -using Microsoft.AspNet.SignalR.Infrastructure; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Serializer; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Datastore.Events; - -namespace NzbDrone.SignalR -{ - public interface IBroadcastSignalRMessage - { - bool IsConnected { get; } - void BroadcastMessage(SignalRMessage message); - } - - public sealed class NzbDronePersistentConnection : PersistentConnection, IBroadcastSignalRMessage - { - private IPersistentConnectionContext Context => ((ConnectionManager)GlobalHost.ConnectionManager).GetConnection(GetType()); - - private static string API_KEY; - private readonly Dictionary _messageHistory; - private HashSet _connections = new HashSet(); - - public NzbDronePersistentConnection(IConfigFileProvider configFileProvider) - { - API_KEY = configFileProvider.ApiKey; - _messageHistory = new Dictionary(); - } - - public bool IsConnected - { - get - { - lock (_connections) - { - return _connections.Count != 0; - } - } - } - - - public void BroadcastMessage(SignalRMessage message) - { - string lastMessage; - if (_messageHistory.TryGetValue(message.Name, out lastMessage)) - { - if (message.Action == ModelAction.Updated && message.Body.ToJson() == lastMessage) - { - return; - } - } - - _messageHistory[message.Name] = message.Body.ToJson(); - - Context.Connection.Broadcast(message); - } - - protected override bool AuthorizeRequest(IRequest request) - { - var apiKey = request.QueryString["apiKey"]; - - if (apiKey.IsNotNullOrWhiteSpace() && apiKey.Equals(API_KEY)) - { - return true; - } - - return false; - } - - protected override Task OnConnected(IRequest request, string connectionId) - { - lock (_connections) - { - _connections.Add(connectionId); - } - - return SendVersion(connectionId); - } - - protected override Task OnReconnected(IRequest request, string connectionId) - { - lock (_connections) - { - _connections.Add(connectionId); - } - - return SendVersion(connectionId); - } - - protected override Task OnDisconnected(IRequest request, string connectionId, bool stopCalled) - { - lock (_connections) - { - _connections.Remove(connectionId); - } - - return base.OnDisconnected(request, connectionId, stopCalled); - } - - private Task SendVersion(string connectionId) - { - return Context.Connection.Send(connectionId, new SignalRMessage - { - Name = "version", - Body = new - { - Version = BuildInfo.Version.ToString() - } - }); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.SignalR/SignalRContractResolver.cs b/src/NzbDrone.SignalR/SignalRContractResolver.cs deleted file mode 100644 index 1ece92321..000000000 --- a/src/NzbDrone.SignalR/SignalRContractResolver.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using Newtonsoft.Json.Serialization; - -namespace NzbDrone.SignalR -{ - public class SignalRContractResolver : IContractResolver - { - private readonly IContractResolver _camelCaseContractResolver; - private readonly IContractResolver _defaultContractSerializer; - - public SignalRContractResolver() - { - _defaultContractSerializer = new DefaultContractResolver(); - _camelCaseContractResolver = new CamelCasePropertyNamesContractResolver(); - } - - public JsonContract ResolveContract(Type type) - { - var fullName = type.FullName; - if (fullName.StartsWith("NzbDrone") || fullName.StartsWith("Sonarr")) - { - return _camelCaseContractResolver.ResolveContract(type); - } - - return _defaultContractSerializer.ResolveContract(type); - } - } -} diff --git a/src/NzbDrone.SignalR/SignalRJsonSerializer.cs b/src/NzbDrone.SignalR/SignalRJsonSerializer.cs deleted file mode 100644 index f86795b90..000000000 --- a/src/NzbDrone.SignalR/SignalRJsonSerializer.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Microsoft.AspNet.SignalR; -using Newtonsoft.Json; -using NzbDrone.Common.Serializer; - -namespace NzbDrone.SignalR -{ - public static class SignalRJsonSerializer - { - private static JsonSerializer _serializer; - private static JsonSerializerSettings _serializerSettings; - - public static void Register() - { - _serializerSettings = Json.GetSerializerSettings(); - _serializerSettings.ContractResolver = new SignalRContractResolver(); - _serializerSettings.Formatting = Formatting.None; // ServerSentEvents doesn't like newlines - - _serializer = JsonSerializer.Create(_serializerSettings); - - GlobalHost.DependencyResolver.Register(typeof(JsonSerializer), () => _serializer); - } - } -} diff --git a/src/NzbDrone.SignalR/SignalrDependencyResolver.cs b/src/NzbDrone.SignalR/SignalrDependencyResolver.cs deleted file mode 100644 index f9a4eec07..000000000 --- a/src/NzbDrone.SignalR/SignalrDependencyResolver.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using Microsoft.AspNet.SignalR; -using Microsoft.AspNet.SignalR.Infrastructure; -using NzbDrone.Common.Composition; - -namespace NzbDrone.SignalR -{ - public class SignalRDependencyResolver : DefaultDependencyResolver - { - private readonly IContainer _container; - - public static void Register(IContainer container) - { - GlobalHost.DependencyResolver = new SignalRDependencyResolver(container); - } - - private SignalRDependencyResolver(IContainer container) - { - _container = container; - var performanceCounterManager = new SonarrPerformanceCounterManager(); - Register(typeof(IPerformanceCounterManager), () => performanceCounterManager); - } - - public override object GetService(Type serviceType) - { - // Microsoft.AspNet.SignalR.Infrastructure.AckSubscriber is not registered in our internal contaiiner, - // but it still gets treated like it is (possibly due to being a concrete type). - - var fullName = serviceType.FullName; - - if (fullName == "Microsoft.AspNet.SignalR.Infrastructure.AckSubscriber" || - fullName == "Newtonsoft.Json.JsonSerializer") - { - return base.GetService(serviceType); - } - - if (_container.IsTypeRegistered(serviceType)) - { - return _container.Resolve(serviceType); - } - - return base.GetService(serviceType); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.SignalR/Sonarr.SignalR.csproj b/src/NzbDrone.SignalR/Sonarr.SignalR.csproj index b9f445267..772271e8d 100644 --- a/src/NzbDrone.SignalR/Sonarr.SignalR.csproj +++ b/src/NzbDrone.SignalR/Sonarr.SignalR.csproj @@ -4,14 +4,10 @@ x86 - - - - - - + + diff --git a/src/NzbDrone.SignalR/SonarrPerformanceCounterManager.cs b/src/NzbDrone.SignalR/SonarrPerformanceCounterManager.cs deleted file mode 100644 index ca5fcf386..000000000 --- a/src/NzbDrone.SignalR/SonarrPerformanceCounterManager.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Threading; -using Microsoft.AspNet.SignalR.Infrastructure; - -namespace NzbDrone.SignalR -{ - public class SonarrPerformanceCounterManager : IPerformanceCounterManager - { - private readonly IPerformanceCounter _counter = new NoOpPerformanceCounter(); - - public void Initialize(string instanceName, CancellationToken hostShutdownToken) - { - - } - - public IPerformanceCounter LoadCounter(string categoryName, string counterName, string instanceName, bool isReadOnly) - { - return _counter; - } - - public IPerformanceCounter ConnectionsConnected => _counter; - public IPerformanceCounter ConnectionsReconnected => _counter; - public IPerformanceCounter ConnectionsDisconnected => _counter; - public IPerformanceCounter ConnectionsCurrent => _counter; - public IPerformanceCounter ConnectionMessagesReceivedTotal => _counter; - public IPerformanceCounter ConnectionMessagesSentTotal => _counter; - public IPerformanceCounter ConnectionMessagesReceivedPerSec => _counter; - public IPerformanceCounter ConnectionMessagesSentPerSec => _counter; - public IPerformanceCounter MessageBusMessagesReceivedTotal => _counter; - public IPerformanceCounter MessageBusMessagesReceivedPerSec => _counter; - public IPerformanceCounter ScaleoutMessageBusMessagesReceivedPerSec => _counter; - public IPerformanceCounter MessageBusMessagesPublishedTotal => _counter; - public IPerformanceCounter MessageBusMessagesPublishedPerSec => _counter; - public IPerformanceCounter MessageBusSubscribersCurrent => _counter; - public IPerformanceCounter MessageBusSubscribersTotal => _counter; - public IPerformanceCounter MessageBusSubscribersPerSec => _counter; - public IPerformanceCounter MessageBusAllocatedWorkers => _counter; - public IPerformanceCounter MessageBusBusyWorkers => _counter; - public IPerformanceCounter MessageBusTopicsCurrent => _counter; - public IPerformanceCounter ErrorsAllTotal => _counter; - public IPerformanceCounter ErrorsAllPerSec => _counter; - public IPerformanceCounter ErrorsHubResolutionTotal => _counter; - public IPerformanceCounter ErrorsHubResolutionPerSec => _counter; - public IPerformanceCounter ErrorsHubInvocationTotal => _counter; - public IPerformanceCounter ErrorsHubInvocationPerSec => _counter; - public IPerformanceCounter ErrorsTransportTotal => _counter; - public IPerformanceCounter ErrorsTransportPerSec => _counter; - public IPerformanceCounter ScaleoutStreamCountTotal => _counter; - public IPerformanceCounter ScaleoutStreamCountOpen => _counter; - public IPerformanceCounter ScaleoutStreamCountBuffering => _counter; - public IPerformanceCounter ScaleoutErrorsTotal => _counter; - public IPerformanceCounter ScaleoutErrorsPerSec => _counter; - public IPerformanceCounter ScaleoutSendQueueLength => _counter; - public IPerformanceCounter ConnectionsCurrentForeverFrame => _counter; - public IPerformanceCounter ConnectionsCurrentLongPolling => _counter; - public IPerformanceCounter ConnectionsCurrentServerSentEvents => _counter; - public IPerformanceCounter ConnectionsCurrentWebSockets => _counter; - } -} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Config/HostConfigModule.cs b/src/Sonarr.Api.V3/Config/HostConfigModule.cs index 10d74562c..c72646a6c 100644 --- a/src/Sonarr.Api.V3/Config/HostConfigModule.cs +++ b/src/Sonarr.Api.V3/Config/HostConfigModule.cs @@ -1,8 +1,8 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Security.Cryptography.X509Certificates; using FluentValidation; -using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; @@ -19,7 +19,10 @@ namespace Sonarr.Api.V3.Config private readonly IConfigService _configService; private readonly IUserService _userService; - public HostConfigModule(IConfigFileProvider configFileProvider, IConfigService configService, IUserService userService) + public HostConfigModule(IConfigFileProvider configFileProvider, + IConfigService configService, + IUserService userService, + FileExistsValidator fileExistsValidator) : base("/config/host") { _configFileProvider = configFileProvider; @@ -45,7 +48,14 @@ namespace Sonarr.Api.V3.Config SharedValidator.RuleFor(c => c.SslPort).ValidPort().When(c => c.EnableSsl); SharedValidator.RuleFor(c => c.SslPort).NotEqual(c => c.Port).When(c => c.EnableSsl); - SharedValidator.RuleFor(c => c.SslCertHash).NotEmpty().When(c => c.EnableSsl && OsInfo.IsWindows); + + SharedValidator.RuleFor(c => c.SslCertPath) + .Cascade(CascadeMode.StopOnFirstFailure) + .NotEmpty() + .IsValidPath() + .SetValidator(fileExistsValidator) + .Must((resource, path) => IsValidSslCertificate(resource)).WithMessage("Invalid SSL certificate file or password") + .When(c => c.EnableSsl); SharedValidator.RuleFor(c => c.Branch).NotEmpty().WithMessage("Branch name is required, 'master' is the default"); SharedValidator.RuleFor(c => c.UpdateScriptPath).IsValidPath().When(c => c.UpdateMechanism == UpdateMechanism.Script); @@ -53,7 +63,21 @@ namespace Sonarr.Api.V3.Config SharedValidator.RuleFor(c => c.BackupFolder).IsValidPath().When(c => Path.IsPathRooted(c.BackupFolder)); SharedValidator.RuleFor(c => c.BackupInterval).InclusiveBetween(1, 7); SharedValidator.RuleFor(c => c.BackupRetention).InclusiveBetween(1, 90); + } + private bool IsValidSslCertificate(HostConfigResource resource) + { + X509Certificate2 cert; + try + { + cert = new X509Certificate2(resource.SslCertPath, resource.SslCertPassword, X509KeyStorageFlags.DefaultKeySet); + } + catch + { + return false; + } + + return cert != null; } private HostConfigResource GetHostConfig() diff --git a/src/Sonarr.Api.V3/Config/HostConfigResource.cs b/src/Sonarr.Api.V3/Config/HostConfigResource.cs index ff3bc777f..30c6284f4 100644 --- a/src/Sonarr.Api.V3/Config/HostConfigResource.cs +++ b/src/Sonarr.Api.V3/Config/HostConfigResource.cs @@ -22,7 +22,8 @@ namespace Sonarr.Api.V3.Config public string ConsoleLogLevel { get; set; } public string Branch { get; set; } public string ApiKey { get; set; } - public string SslCertHash { get; set; } + public string SslCertPath { get; set; } + public string SslCertPassword { get; set; } public string UrlBase { get; set; } public string InstanceName { get; set; } public bool UpdateAutomatically { get; set; } @@ -62,7 +63,8 @@ namespace Sonarr.Api.V3.Config ConsoleLogLevel = model.ConsoleLogLevel, Branch = model.Branch, ApiKey = model.ApiKey, - SslCertHash = model.SslCertHash, + SslCertPath = model.SslCertPath, + SslCertPassword = model.SslCertPassword, UrlBase = model.UrlBase, InstanceName = model.InstanceName, UpdateAutomatically = model.UpdateAutomatically, diff --git a/yarn.lock b/yarn.lock index da2621eda..cd5a244cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1017,6 +1017,17 @@ dependencies: prop-types "^15.7.2" +"@microsoft/signalr@5.0.5": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@microsoft/signalr/-/signalr-5.0.5.tgz#817d577d76aab33548f1354c72d779a18cc770e2" + integrity sha512-1aIr9LfuVHkJA6YHvJ9+V2GPUOlVtH94babg4LmBHk3tO7bI9YDHz3axYsp/GI5MVMqCKg/7BzEorr6zs/w2XA== + dependencies: + abort-controller "^3.0.0" + eventsource "^1.0.7" + fetch-cookie "^0.7.3" + node-fetch "^2.6.0" + ws "^6.0.0" + "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" @@ -1448,6 +1459,13 @@ JSONStream@^1.3.5: jsonparse "^1.2.0" through ">=2.2.7 <3" +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + acorn-jsx@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" @@ -1675,6 +1693,11 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== +async-limiter@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + async@^2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" @@ -2262,12 +2285,16 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -connected-react-router@6.8.0: - version "6.8.0" - resolved "https://registry.yarnpkg.com/connected-react-router/-/connected-react-router-6.8.0.tgz#ddc687b31d498322445d235d660798489fa56cae" - integrity sha512-E64/6krdJM3Ag3MMmh2nKPtMbH15s3JQDuaYJvOVXzu6MbHbDyIvuwLOyhQIuP4Om9zqEfZYiVyflROibSsONg== +connected-react-router@6.9.1: + version "6.9.1" + resolved "https://registry.yarnpkg.com/connected-react-router/-/connected-react-router-6.9.1.tgz#d842eebaa15b9920e2e45fc03d74e41110e94e4c" + integrity sha512-BbtB6t0iqAwGwygDenJl9zmlk7vpKWIRSycULmkAOn2RUaF6+bqETprl0qcIqQmY5CTqSwKanaxkLXYWiffAfQ== dependencies: + lodash.isequalwith "^4.4.0" prop-types "^15.7.2" + optionalDependencies: + immutable "^3.8.1 || ^4.0.0-rc.1" + seamless-immutable "^7.1.3" continuable-cache@^0.3.1: version "0.3.1" @@ -2366,14 +2393,6 @@ crc32-stream@^4.0.1: crc-32 "^1.2.0" readable-stream "^3.4.0" -create-react-class@15.7.0: - version "15.7.0" - resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.7.0.tgz#7499d7ca2e69bb51d13faf59bd04f0c65a1d6c1e" - integrity sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng== - dependencies: - loose-envify "^1.3.1" - object-assign "^4.1.1" - create-react-context@<=0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.2.2.tgz#9836542f9aaa22868cd7d4a6f82667df38019dca" @@ -2789,6 +2808,11 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +es6-denodeify@^0.1.1: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-denodeify/-/es6-denodeify-0.1.5.tgz#31d4d5fe9c5503e125460439310e16a2a3f39c1f" + integrity sha1-MdTV/pxVA+ElRgQ5MQ4WoqPznB8= + es6-promise@^4.0.3, es6-promise@^4.2.8: version "4.2.8" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" @@ -3004,11 +3028,23 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + events@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== +eventsource@^1.0.7: + version "1.1.0" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.1.0.tgz#00e8ca7c92109e94b0ddf32dac677d841028cfaf" + integrity sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg== + dependencies: + original "^1.0.0" + exec-sh@^0.3.2: version "0.3.6" resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.6.tgz#ff264f9e325519a60cb5e273692943483cca63bc" @@ -3184,6 +3220,14 @@ fbjs@^0.8.0: setimmediate "^1.0.5" ua-parser-js "^0.7.18" +fetch-cookie@^0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/fetch-cookie/-/fetch-cookie-0.7.3.tgz#b8d023f421dd2b2f4a0eca9cd7318a967ed4eed8" + integrity sha512-rZPkLnI8x5V+zYAiz8QonAHsTb4BY+iFowFBI1RFn0zrO343AVp9X7/yUj/9wL6Ef/8fLls8b/vGtzUvmyAUGA== + dependencies: + es6-denodeify "^0.1.1" + tough-cookie "^2.3.3" + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -3719,6 +3763,11 @@ immediate@~3.0.5: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= +"immutable@^3.8.1 || ^4.0.0-rc.1": + version "4.0.0-rc.14" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0-rc.14.tgz#29ba96631ec10867d1348515ac4e6bdba462f071" + integrity sha512-pfkvmRKJSoW7JFx0QeYlAmT+kNYvn5j0u7bnpNq4N2RCvHSTlLT208G8jgaquNe+Q8kCPHKOSpxJkyvLDpYq0w== + import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -4108,7 +4157,7 @@ jest-worker@^26.6.2: merge-stream "^2.0.0" supports-color "^7.0.0" -jquery@3.6.0, jquery@>=1.6.4: +jquery@3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.0.tgz#c72a09f15c1bdce142f49dbf1170bdf8adac2470" integrity sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw== @@ -4355,6 +4404,11 @@ lodash.flatten@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= +lodash.isequalwith@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.isequalwith/-/lodash.isequalwith-4.4.0.tgz#266726ddd528f854f21f4ea98a065606e0fbc6b0" + integrity sha1-Jmcm3dUo+FTyH06pigZWBuD7xrA= + lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" @@ -4734,6 +4788,11 @@ node-fetch@^1.0.1: encoding "^0.1.11" is-stream "^1.0.1" +node-fetch@^2.6.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" + integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -4952,6 +5011,13 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" +original@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" + integrity sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg== + dependencies: + url-parse "^1.4.3" + os-homedir@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" @@ -5430,6 +5496,11 @@ protochain@^1.0.5: resolved "https://registry.yarnpkg.com/protochain/-/protochain-1.0.5.tgz#991c407e99de264aadf8f81504b5e7faf7bfa260" integrity sha1-mRxAfpneJkqt+PgVBLXn+ve/omA= +psl@^1.1.28: + version "1.8.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" + integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== + pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -5438,7 +5509,7 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@^2.1.0: +punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== @@ -5450,6 +5521,11 @@ qs@6.10.1, qs@^6.4.0: dependencies: side-channel "^1.0.4" +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -5989,6 +6065,11 @@ require-nocache@1.0.0: resolved "https://registry.yarnpkg.com/require-nocache/-/require-nocache-1.0.0.tgz#a665d0b60a07e8249875790a4d350219d3c85fa3" integrity sha1-pmXQtgoH6CSYdXkKTTUCGdPIX6M= +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= + reselect@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" @@ -6193,6 +6274,11 @@ schema-utils@^2.6.5: ajv "^6.12.4" ajv-keywords "^3.5.2" +seamless-immutable@^7.1.3: + version "7.1.4" + resolved "https://registry.yarnpkg.com/seamless-immutable/-/seamless-immutable-7.1.4.tgz#6e9536def083ddc4dea0207d722e0e80d0f372f8" + integrity sha512-XiUO1QP4ki4E2PHegiGAlu6r82o5A+6tRh7IkGGTVg/h+UoeX4nFBeCGPOhb4CYjvkqsfm/TUtvOMYC1xmV30A== + section-iterator@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/section-iterator/-/section-iterator-2.0.0.tgz#bf444d7afeeb94ad43c39ad2fb26151627ccba2a" @@ -6314,13 +6400,6 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== -signalr@2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/signalr/-/signalr-2.4.2.tgz#8da61c66ca1d29c4439ce1a296300f1b64d53ee6" - integrity sha512-XqFRQRbRr8Ce1GYq3/aWwnQKPHWEOTjmRT32196sZEmZQmsEEpu5LzjvxrpxxedUoI/oWu9YlqkqK8KrbSYw/w== - dependencies: - jquery ">=1.6.4" - slash@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" @@ -6851,6 +6930,14 @@ to-space-case@^1.0.0: dependencies: to-no-case "^1.0.0" +tough-cookie@^2.3.3: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + trim-newlines@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30" @@ -7031,6 +7118,14 @@ url-loader@4.1.1: mime-types "^2.1.27" schema-utils "^3.0.0" +url-parse@^1.4.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862" + integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + use-callback-ref@^1.2.1: version "1.2.5" resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.5.tgz#6115ed242cfbaed5915499c0a9842ca2912f38a5" @@ -7308,6 +7403,13 @@ write-file-atomic@^3.0.3: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" +ws@^6.0.0: + version "6.2.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e" + integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw== + dependencies: + async-limiter "~1.0.0" + xxhashjs@~0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/xxhashjs/-/xxhashjs-0.2.2.tgz#8a6251567621a1c46a5ae204da0249c7f8caa9d8"