diff --git a/src/Renci.SshNet/.editorconfig b/src/Renci.SshNet/.editorconfig index c5f4bdf61..fce5e66b4 100644 --- a/src/Renci.SshNet/.editorconfig +++ b/src/Renci.SshNet/.editorconfig @@ -159,3 +159,7 @@ dotnet_diagnostic.IDE0048.severity = none # IDE0305: Collection initialization can be simplified # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0305 dotnet_diagnostic.IDE0305.severity = none + +# IDE0005: Remove unnecessary using directives +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0005 +dotnet_diagnostic.IDE0005.severity = suggestion diff --git a/src/Renci.SshNet/Abstractions/SocketAbstraction.cs b/src/Renci.SshNet/Abstractions/SocketAbstraction.cs index 24dceb3d0..e1a12362e 100644 --- a/src/Renci.SshNet/Abstractions/SocketAbstraction.cs +++ b/src/Renci.SshNet/Abstractions/SocketAbstraction.cs @@ -129,7 +129,7 @@ public static void ClearReadBuffer(Socket socket) public static int ReadPartial(Socket socket, byte[] buffer, int offset, int size, TimeSpan timeout) { - socket.ReceiveTimeout = (int) timeout.TotalMilliseconds; + socket.ReceiveTimeout = timeout.AsTimeout(nameof(timeout)); try { @@ -274,7 +274,7 @@ public static int Read(Socket socket, byte[] buffer, int offset, int size, TimeS var totalBytesRead = 0; var totalBytesToRead = size; - socket.ReceiveTimeout = (int) readTimeout.TotalMilliseconds; + socket.ReceiveTimeout = readTimeout.AsTimeout(nameof(readTimeout)); do { diff --git a/src/Renci.SshNet/BaseClient.cs b/src/Renci.SshNet/BaseClient.cs index 56d2145cc..9d60d84ea 100644 --- a/src/Renci.SshNet/BaseClient.cs +++ b/src/Renci.SshNet/BaseClient.cs @@ -101,6 +101,8 @@ public TimeSpan KeepAliveInterval { CheckDisposed(); + value.EnsureValidTimeout(nameof(KeepAliveInterval)); + if (value == _keepAliveInterval) { return; diff --git a/src/Renci.SshNet/Common/TimeSpanExtensions.cs b/src/Renci.SshNet/Common/TimeSpanExtensions.cs new file mode 100644 index 000000000..39470ef4f --- /dev/null +++ b/src/Renci.SshNet/Common/TimeSpanExtensions.cs @@ -0,0 +1,46 @@ +using System; + +namespace Renci.SshNet.Common +{ + /// + /// Provides extension methods for . + /// + internal static class TimeSpanExtensions + { + private const string OutOfRangeTimeoutMessage = + $"The timeout must represent a value between -1 and Int32.MaxValue milliseconds, inclusive."; + + /// + /// Returns the specified as a valid timeout in milliseconds. + /// + /// The to ensure validity. + /// The name of the calling member. + /// + /// Thrown when does not represent a value between -1 and , inclusive. + /// + public static int AsTimeout(this TimeSpan timeSpan, string callerMemberName) + { + var timeoutInMilliseconds = timeSpan.TotalMilliseconds; + return timeoutInMilliseconds is < -1d or > int.MaxValue + ? throw new ArgumentOutOfRangeException(callerMemberName, OutOfRangeTimeoutMessage) + : (int) timeoutInMilliseconds; + } + + /// + /// Ensures that the specified represents a valid timeout in milliseconds. + /// + /// The to ensure validity. + /// The name of the calling member. + /// + /// Thrown when does not represent a value between -1 and , inclusive. + /// + public static void EnsureValidTimeout(this TimeSpan timeSpan, string callerMemberName) + { + var timeoutInMilliseconds = timeSpan.TotalMilliseconds; + if (timeoutInMilliseconds is < -1d or > int.MaxValue) + { + throw new ArgumentOutOfRangeException(callerMemberName, OutOfRangeTimeoutMessage); + } + } + } +} diff --git a/src/Renci.SshNet/ConnectionInfo.cs b/src/Renci.SshNet/ConnectionInfo.cs index 3fc6da1be..7584816ff 100644 --- a/src/Renci.SshNet/ConnectionInfo.cs +++ b/src/Renci.SshNet/ConnectionInfo.cs @@ -44,6 +44,9 @@ public class ConnectionInfo : IConnectionInfoInternal /// private static readonly TimeSpan DefaultChannelCloseTimeout = TimeSpan.FromSeconds(1); + private TimeSpan _timeout; + private TimeSpan _channelCloseTimeout; + /// /// Gets supported key exchange algorithms for this connection. /// @@ -145,7 +148,19 @@ public class ConnectionInfo : IConnectionInfoInternal /// /// The connection timeout. The default value is 30 seconds. /// - public TimeSpan Timeout { get; set; } + public TimeSpan Timeout + { + get + { + return _timeout; + } + set + { + value.EnsureValidTimeout(nameof(Timeout)); + + _timeout = value; + } + } /// /// Gets or sets the timeout to use when waiting for a server to acknowledge closing a channel. @@ -157,7 +172,19 @@ public class ConnectionInfo : IConnectionInfoInternal /// If a server does not send a SSH_MSG_CHANNEL_CLOSE message before the specified timeout /// elapses, the channel will be closed immediately. /// - public TimeSpan ChannelCloseTimeout { get; set; } + public TimeSpan ChannelCloseTimeout + { + get + { + return _channelCloseTimeout; + } + set + { + value.EnsureValidTimeout(nameof(ChannelCloseTimeout)); + + _channelCloseTimeout = value; + } + } /// /// Gets or sets the character encoding. diff --git a/src/Renci.SshNet/ForwardedPort.cs b/src/Renci.SshNet/ForwardedPort.cs index 50157e1c1..b0b5838c8 100644 --- a/src/Renci.SshNet/ForwardedPort.cs +++ b/src/Renci.SshNet/ForwardedPort.cs @@ -102,6 +102,8 @@ public void Dispose() /// The maximum amount of time to wait for pending requests to finish processing. protected virtual void StopPort(TimeSpan timeout) { + timeout.EnsureValidTimeout(nameof(timeout)); + RaiseClosing(); var session = Session; diff --git a/src/Renci.SshNet/ForwardedPortDynamic.cs b/src/Renci.SshNet/ForwardedPortDynamic.cs index 0c6e417a8..2a2c45f2c 100644 --- a/src/Renci.SshNet/ForwardedPortDynamic.cs +++ b/src/Renci.SshNet/ForwardedPortDynamic.cs @@ -101,6 +101,8 @@ protected override void StartPort() /// The maximum amount of time to wait for pending requests to finish processing. protected override void StopPort(TimeSpan timeout) { + timeout.EnsureValidTimeout(nameof(timeout)); + if (!ForwardedPortStatus.ToStopping(ref _status)) { return; diff --git a/src/Renci.SshNet/ForwardedPortLocal.cs b/src/Renci.SshNet/ForwardedPortLocal.cs index fce8f7fd7..2b0d678e4 100644 --- a/src/Renci.SshNet/ForwardedPortLocal.cs +++ b/src/Renci.SshNet/ForwardedPortLocal.cs @@ -138,6 +138,8 @@ protected override void StartPort() /// The maximum amount of time to wait for pending requests to finish processing. protected override void StopPort(TimeSpan timeout) { + timeout.EnsureValidTimeout(nameof(timeout)); + if (!ForwardedPortStatus.ToStopping(ref _status)) { return; diff --git a/src/Renci.SshNet/ForwardedPortRemote.cs b/src/Renci.SshNet/ForwardedPortRemote.cs index 10430abc3..183b220fe 100644 --- a/src/Renci.SshNet/ForwardedPortRemote.cs +++ b/src/Renci.SshNet/ForwardedPortRemote.cs @@ -188,6 +188,8 @@ protected override void StartPort() /// The maximum amount of time to wait for the port to stop. protected override void StopPort(TimeSpan timeout) { + timeout.EnsureValidTimeout(nameof(timeout)); + if (!ForwardedPortStatus.ToStopping(ref _status)) { return; diff --git a/src/Renci.SshNet/NetConfClient.cs b/src/Renci.SshNet/NetConfClient.cs index 09cff7609..a15a99e58 100644 --- a/src/Renci.SshNet/NetConfClient.cs +++ b/src/Renci.SshNet/NetConfClient.cs @@ -36,13 +36,7 @@ public TimeSpan OperationTimeout } set { - var timeoutInMilliseconds = value.TotalMilliseconds; - if (timeoutInMilliseconds is < -1d or > int.MaxValue) - { - throw new ArgumentOutOfRangeException(nameof(value), "The timeout must represent a value between -1 and Int32.MaxValue, inclusive."); - } - - _operationTimeout = (int) timeoutInMilliseconds; + _operationTimeout = value.AsTimeout(nameof(OperationTimeout)); } } diff --git a/src/Renci.SshNet/ScpClient.cs b/src/Renci.SshNet/ScpClient.cs index 8121c9702..507cf0a04 100644 --- a/src/Renci.SshNet/ScpClient.cs +++ b/src/Renci.SshNet/ScpClient.cs @@ -38,6 +38,7 @@ public partial class ScpClient : BaseClient private static readonly Regex TimestampRe = new Regex(@"T(?\d+) 0 (?\d+) 0", RegexOptions.Compiled); private IRemotePathTransformation _remotePathTransformation; + private TimeSpan _operationTimeout; /// /// Gets or sets the operation timeout. @@ -46,7 +47,19 @@ public partial class ScpClient : BaseClient /// The timeout to wait until an operation completes. The default value is negative /// one (-1) milliseconds, which indicates an infinite time-out period. /// - public TimeSpan OperationTimeout { get; set; } + public TimeSpan OperationTimeout + { + get + { + return _operationTimeout; + } + set + { + value.EnsureValidTimeout(nameof(OperationTimeout)); + + _operationTimeout = value; + } + } /// /// Gets or sets the size of the buffer. diff --git a/src/Renci.SshNet/Sftp/SftpFileStream.cs b/src/Renci.SshNet/Sftp/SftpFileStream.cs index 7880ffbbd..312dd9952 100644 --- a/src/Renci.SshNet/Sftp/SftpFileStream.cs +++ b/src/Renci.SshNet/Sftp/SftpFileStream.cs @@ -35,6 +35,7 @@ public class SftpFileStream : Stream private bool _canRead; private bool _canSeek; private bool _canWrite; + private TimeSpan _timeout; /// /// Gets a value indicating whether the current stream supports reading. @@ -176,7 +177,19 @@ public virtual byte[] Handle /// /// The timeout. /// - public TimeSpan Timeout { get; set; } + public TimeSpan Timeout + { + get + { + return _timeout; + } + set + { + value.EnsureValidTimeout(nameof(Timeout)); + + _timeout = value; + } + } private SftpFileStream(ISftpSession session, string path, FileAccess access, int bufferSize, byte[] handle, long position) { diff --git a/src/Renci.SshNet/SftpClient.cs b/src/Renci.SshNet/SftpClient.cs index 01472dbff..7d2c62165 100644 --- a/src/Renci.SshNet/SftpClient.cs +++ b/src/Renci.SshNet/SftpClient.cs @@ -59,13 +59,7 @@ public TimeSpan OperationTimeout { CheckDisposed(); - var timeoutInMilliseconds = value.TotalMilliseconds; - if (timeoutInMilliseconds is < -1d or > int.MaxValue) - { - throw new ArgumentOutOfRangeException(nameof(value), "The timeout must represent a value between -1 and Int32.MaxValue, inclusive."); - } - - _operationTimeout = (int) timeoutInMilliseconds; + _operationTimeout = value.AsTimeout(nameof(OperationTimeout)); } } diff --git a/src/Renci.SshNet/SshCommand.cs b/src/Renci.SshNet/SshCommand.cs index 7aa013ad8..b348c4ec9 100644 --- a/src/Renci.SshNet/SshCommand.cs +++ b/src/Renci.SshNet/SshCommand.cs @@ -32,6 +32,7 @@ public class SshCommand : IDisposable private bool _hasError; private bool _isDisposed; private ChannelInputStream _inputStream; + private TimeSpan _commandTimeout; /// /// Gets the command text. @@ -44,7 +45,19 @@ public class SshCommand : IDisposable /// /// The command timeout. /// - public TimeSpan CommandTimeout { get; set; } + public TimeSpan CommandTimeout + { + get + { + return _commandTimeout; + } + set + { + value.EnsureValidTimeout(nameof(CommandTimeout)); + + _commandTimeout = value; + } + } /// /// Gets the command exit status. diff --git a/test/Renci.SshNet.Tests/Classes/Common/TimeSpanExtensionsTest.cs b/test/Renci.SshNet.Tests/Classes/Common/TimeSpanExtensionsTest.cs new file mode 100644 index 000000000..71f91c8ee --- /dev/null +++ b/test/Renci.SshNet.Tests/Classes/Common/TimeSpanExtensionsTest.cs @@ -0,0 +1,103 @@ +using System; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Renci.SshNet.Common; +using Renci.SshNet.Tests.Common; + +namespace Renci.SshNet.Tests.Classes.Common +{ + [TestClass] + public class TimeSpanExtensionsTest + { + [TestMethod] + public void AsTimeout_ValidTimeSpan_ReturnsExpectedMilliseconds() + { + var timeSpan = TimeSpan.FromSeconds(10); + + var timeout = timeSpan.AsTimeout("TestMethodName"); + + Assert.AreEqual(10000, timeout); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AsTimeout_NegativeTimeSpan_ThrowsArgumentOutOfRangeException() + { + var timeSpan = TimeSpan.FromSeconds(-1); + + timeSpan.AsTimeout("TestMethodName"); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AsTimeout_TimeSpanExceedingMaxValue_ThrowsArgumentOutOfRangeException() + { + var timeSpan = TimeSpan.FromMilliseconds((double) int.MaxValue + 1); + + timeSpan.AsTimeout("TestMethodName"); + } + + [TestMethod] + public void AsTimeout_ArgumentOutOfRangeException_HasCorrectInformation() + { + + try + { + var timeSpan = TimeSpan.FromMilliseconds((double) int.MaxValue + 1); + + timeSpan.AsTimeout("TestMethodName"); + } + catch (ArgumentOutOfRangeException ex) + { + Assert.IsNull(ex.InnerException); + ArgumentExceptionAssert.MessageEquals("The timeout must represent a value between -1 and Int32.MaxValue milliseconds, inclusive.", ex); + Assert.AreEqual("TestMethodName", ex.ParamName); + } + } + + [TestMethod] + public void EnsureValidTimeout_ValidTimeSpan_DoesNotThrow() + { + var timeSpan = TimeSpan.FromSeconds(5); + + timeSpan.EnsureValidTimeout("TestMethodName"); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void EnsureValidTimeout_NegativeTimeSpan_ThrowsArgumentOutOfRangeException() + { + var timeSpan = TimeSpan.FromSeconds(-1); + + timeSpan.EnsureValidTimeout("TestMethodName"); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void EnsureValidTimeout_TimeSpanExceedingMaxValue_ThrowsArgumentOutOfRangeException() + { + var timeSpan = TimeSpan.FromMilliseconds((double) int.MaxValue + 1); + + timeSpan.EnsureValidTimeout("TestMethodName"); + } + + [TestMethod] + public void EnsureValidTimeout_ArgumentOutOfRangeException_HasCorrectInformation() + { + + try + { + var timeSpan = TimeSpan.FromMilliseconds((double) int.MaxValue + 1); + + timeSpan.EnsureValidTimeout("TestMethodName"); + } + catch (ArgumentOutOfRangeException ex) + { + Assert.IsNull(ex.InnerException); + ArgumentExceptionAssert.MessageEquals("The timeout must represent a value between -1 and Int32.MaxValue milliseconds, inclusive.", ex); + Assert.AreEqual("TestMethodName", ex.ParamName); + } + } + } +} diff --git a/test/Renci.SshNet.Tests/Classes/ConnectionInfoTest.cs b/test/Renci.SshNet.Tests/Classes/ConnectionInfoTest.cs index c6b19ca49..130b4b534 100644 --- a/test/Renci.SshNet.Tests/Classes/ConnectionInfoTest.cs +++ b/test/Renci.SshNet.Tests/Classes/ConnectionInfoTest.cs @@ -275,6 +275,84 @@ public void Test_ConnectionInfo_Port_Valid() Assert.AreEqual(port, connectionInfo.Port); } + [TestMethod] + [TestCategory("ConnectionInfo")] + public void Test_ConnectionInfo_Timeout_Valid() + { + var connectionInfo = new ConnectionInfo(Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, ProxyTypes.None, + Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, + Resources.PASSWORD, new KeyboardInteractiveAuthenticationMethod(Resources.USERNAME)); + + try + { + connectionInfo.Timeout = TimeSpan.FromMilliseconds(-2); + } + catch (ArgumentOutOfRangeException ex) + { + Assert.IsNull(ex.InnerException); + ArgumentExceptionAssert.MessageEquals("The timeout must represent a value between -1 and Int32.MaxValue milliseconds, inclusive.", ex); + + Assert.AreEqual("Timeout", ex.ParamName); + } + + connectionInfo.Timeout = TimeSpan.FromMilliseconds(-1); + Assert.AreEqual(connectionInfo.Timeout, TimeSpan.FromMilliseconds(-1)); + + connectionInfo.Timeout = TimeSpan.FromMilliseconds(int.MaxValue); + Assert.AreEqual(connectionInfo.Timeout, TimeSpan.FromMilliseconds(int.MaxValue)); + + try + { + connectionInfo.Timeout = TimeSpan.FromMilliseconds((double)int.MaxValue + 1); + } + catch (ArgumentOutOfRangeException ex) + { + Assert.IsNull(ex.InnerException); + ArgumentExceptionAssert.MessageEquals("The timeout must represent a value between -1 and Int32.MaxValue milliseconds, inclusive.", ex); + + Assert.AreEqual("Timeout", ex.ParamName); + } + } + + [TestMethod] + [TestCategory("ConnectionInfo")] + public void Test_ConnectionInfo_ChannelCloseTimeout_Valid() + { + var connectionInfo = new ConnectionInfo(Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, ProxyTypes.None, + Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, + Resources.PASSWORD, new KeyboardInteractiveAuthenticationMethod(Resources.USERNAME)); + + try + { + connectionInfo.ChannelCloseTimeout = TimeSpan.FromMilliseconds(-2); + } + catch (ArgumentOutOfRangeException ex) + { + Assert.IsNull(ex.InnerException); + ArgumentExceptionAssert.MessageEquals("The timeout must represent a value between -1 and Int32.MaxValue milliseconds, inclusive.", ex); + + Assert.AreEqual("ChannelCloseTimeout", ex.ParamName); + } + + connectionInfo.ChannelCloseTimeout = TimeSpan.FromMilliseconds(-1); + Assert.AreEqual(connectionInfo.ChannelCloseTimeout, TimeSpan.FromMilliseconds(-1)); + + connectionInfo.ChannelCloseTimeout = TimeSpan.FromMilliseconds(int.MaxValue); + Assert.AreEqual(connectionInfo.ChannelCloseTimeout, TimeSpan.FromMilliseconds(int.MaxValue)); + + try + { + connectionInfo.ChannelCloseTimeout = TimeSpan.FromMilliseconds((double)int.MaxValue + 1); + } + catch (ArgumentOutOfRangeException ex) + { + Assert.IsNull(ex.InnerException); + ArgumentExceptionAssert.MessageEquals("The timeout must represent a value between -1 and Int32.MaxValue milliseconds, inclusive.", ex); + + Assert.AreEqual("ChannelCloseTimeout", ex.ParamName); + } + } + [TestMethod] [TestCategory("ConnectionInfo")] public void ConstructorShouldThrowArgumentExceptionhenUsernameIsNull() diff --git a/test/Renci.SshNet.Tests/Classes/NetConfClientTest.cs b/test/Renci.SshNet.Tests/Classes/NetConfClientTest.cs index d86ca2e37..b67644585 100644 --- a/test/Renci.SshNet.Tests/Classes/NetConfClientTest.cs +++ b/test/Renci.SshNet.Tests/Classes/NetConfClientTest.cs @@ -85,8 +85,9 @@ public void OperationTimeout_LessThanLowerLimit() catch (ArgumentOutOfRangeException ex) { Assert.IsNull(ex.InnerException); - ArgumentExceptionAssert.MessageEquals("The timeout must represent a value between -1 and Int32.MaxValue, inclusive.", ex); - Assert.AreEqual("value", ex.ParamName); + ArgumentExceptionAssert.MessageEquals("The timeout must represent a value between -1 and Int32.MaxValue milliseconds, inclusive.", ex); + + Assert.AreEqual("OperationTimeout", ex.ParamName); } } @@ -104,8 +105,9 @@ public void OperationTimeout_GreaterThanLowerLimit() catch (ArgumentOutOfRangeException ex) { Assert.IsNull(ex.InnerException); - ArgumentExceptionAssert.MessageEquals("The timeout must represent a value between -1 and Int32.MaxValue, inclusive.", ex); - Assert.AreEqual("value", ex.ParamName); + ArgumentExceptionAssert.MessageEquals("The timeout must represent a value between -1 and Int32.MaxValue milliseconds, inclusive.", ex); + + Assert.AreEqual("OperationTimeout", ex.ParamName); } } } diff --git a/test/Renci.SshNet.Tests/Classes/SftpClientTest.cs b/test/Renci.SshNet.Tests/Classes/SftpClientTest.cs index 25624dcc7..37915174a 100644 --- a/test/Renci.SshNet.Tests/Classes/SftpClientTest.cs +++ b/test/Renci.SshNet.Tests/Classes/SftpClientTest.cs @@ -88,8 +88,9 @@ public void OperationTimeout_LessThanLowerLimit() catch (ArgumentOutOfRangeException ex) { Assert.IsNull(ex.InnerException); - ArgumentExceptionAssert.MessageEquals("The timeout must represent a value between -1 and Int32.MaxValue, inclusive.", ex); - Assert.AreEqual("value", ex.ParamName); + ArgumentExceptionAssert.MessageEquals("The timeout must represent a value between -1 and Int32.MaxValue milliseconds, inclusive.", ex); + + Assert.AreEqual("OperationTimeout", ex.ParamName); } } @@ -107,8 +108,9 @@ public void OperationTimeout_GreaterThanLowerLimit() catch (ArgumentOutOfRangeException ex) { Assert.IsNull(ex.InnerException); - ArgumentExceptionAssert.MessageEquals("The timeout must represent a value between -1 and Int32.MaxValue, inclusive.", ex); - Assert.AreEqual("value", ex.ParamName); + ArgumentExceptionAssert.MessageEquals("The timeout must represent a value between -1 and Int32.MaxValue milliseconds, inclusive.", ex); + + Assert.AreEqual("OperationTimeout", ex.ParamName); } }