Skip to content

Commit 4b9c3bf

Browse files
authored
Limit TimeSpan timeouts to Int32 MaxValue (#1321)
* Added guard clauses to various timeouts to ensure they don't exceed an Int32 in milliseconds. * Fixed guard clauses. * Updated build tags. * Added guard clauses to various timeouts to ensure they don't exceed an Int32 in milliseconds. * Fixed tests. * Added additional tests. * Replaced NoWarn with .editorconfig setting * Fixed references to parameter names.
1 parent 664595d commit 4b9c3bf

18 files changed

+328
-29
lines changed

src/Renci.SshNet/.editorconfig

+4
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,7 @@ dotnet_diagnostic.IDE0048.severity = none
159159
# IDE0305: Collection initialization can be simplified
160160
# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0305
161161
dotnet_diagnostic.IDE0305.severity = none
162+
163+
# IDE0005: Remove unnecessary using directives
164+
# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0005
165+
dotnet_diagnostic.IDE0005.severity = suggestion

src/Renci.SshNet/Abstractions/SocketAbstraction.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ public static void ClearReadBuffer(Socket socket)
129129

130130
public static int ReadPartial(Socket socket, byte[] buffer, int offset, int size, TimeSpan timeout)
131131
{
132-
socket.ReceiveTimeout = (int) timeout.TotalMilliseconds;
132+
socket.ReceiveTimeout = timeout.AsTimeout(nameof(timeout));
133133

134134
try
135135
{
@@ -274,7 +274,7 @@ public static int Read(Socket socket, byte[] buffer, int offset, int size, TimeS
274274
var totalBytesRead = 0;
275275
var totalBytesToRead = size;
276276

277-
socket.ReceiveTimeout = (int) readTimeout.TotalMilliseconds;
277+
socket.ReceiveTimeout = readTimeout.AsTimeout(nameof(readTimeout));
278278

279279
do
280280
{

src/Renci.SshNet/BaseClient.cs

+2
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ public TimeSpan KeepAliveInterval
101101
{
102102
CheckDisposed();
103103

104+
value.EnsureValidTimeout(nameof(KeepAliveInterval));
105+
104106
if (value == _keepAliveInterval)
105107
{
106108
return;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using System;
2+
3+
namespace Renci.SshNet.Common
4+
{
5+
/// <summary>
6+
/// Provides extension methods for <see cref="TimeSpan"/>.
7+
/// </summary>
8+
internal static class TimeSpanExtensions
9+
{
10+
private const string OutOfRangeTimeoutMessage =
11+
$"The timeout must represent a value between -1 and Int32.MaxValue milliseconds, inclusive.";
12+
13+
/// <summary>
14+
/// Returns the specified <paramref name="timeSpan"/> as a valid timeout in milliseconds.
15+
/// </summary>
16+
/// <param name="timeSpan">The <see cref="TimeSpan"/> to ensure validity.</param>
17+
/// <param name="callerMemberName">The name of the calling member.</param>
18+
/// <exception cref="ArgumentOutOfRangeException">
19+
/// Thrown when <paramref name="timeSpan"/> does not represent a value between -1 and <see cref="int.MaxValue"/>, inclusive.
20+
/// </exception>
21+
public static int AsTimeout(this TimeSpan timeSpan, string callerMemberName)
22+
{
23+
var timeoutInMilliseconds = timeSpan.TotalMilliseconds;
24+
return timeoutInMilliseconds is < -1d or > int.MaxValue
25+
? throw new ArgumentOutOfRangeException(callerMemberName, OutOfRangeTimeoutMessage)
26+
: (int) timeoutInMilliseconds;
27+
}
28+
29+
/// <summary>
30+
/// Ensures that the specified <paramref name="timeSpan"/> represents a valid timeout in milliseconds.
31+
/// </summary>
32+
/// <param name="timeSpan">The <see cref="TimeSpan"/> to ensure validity.</param>
33+
/// <param name="callerMemberName">The name of the calling member.</param>
34+
/// <exception cref="ArgumentOutOfRangeException">
35+
/// Thrown when <paramref name="timeSpan"/> does not represent a value between -1 and <see cref="int.MaxValue"/>, inclusive.
36+
/// </exception>
37+
public static void EnsureValidTimeout(this TimeSpan timeSpan, string callerMemberName)
38+
{
39+
var timeoutInMilliseconds = timeSpan.TotalMilliseconds;
40+
if (timeoutInMilliseconds is < -1d or > int.MaxValue)
41+
{
42+
throw new ArgumentOutOfRangeException(callerMemberName, OutOfRangeTimeoutMessage);
43+
}
44+
}
45+
}
46+
}

src/Renci.SshNet/ConnectionInfo.cs

+29-2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ public class ConnectionInfo : IConnectionInfoInternal
4444
/// </value>
4545
private static readonly TimeSpan DefaultChannelCloseTimeout = TimeSpan.FromSeconds(1);
4646

47+
private TimeSpan _timeout;
48+
private TimeSpan _channelCloseTimeout;
49+
4750
/// <summary>
4851
/// Gets supported key exchange algorithms for this connection.
4952
/// </summary>
@@ -145,7 +148,19 @@ public class ConnectionInfo : IConnectionInfoInternal
145148
/// <value>
146149
/// The connection timeout. The default value is 30 seconds.
147150
/// </value>
148-
public TimeSpan Timeout { get; set; }
151+
public TimeSpan Timeout
152+
{
153+
get
154+
{
155+
return _timeout;
156+
}
157+
set
158+
{
159+
value.EnsureValidTimeout(nameof(Timeout));
160+
161+
_timeout = value;
162+
}
163+
}
149164

150165
/// <summary>
151166
/// 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
157172
/// If a server does not send a <c>SSH_MSG_CHANNEL_CLOSE</c> message before the specified timeout
158173
/// elapses, the channel will be closed immediately.
159174
/// </remarks>
160-
public TimeSpan ChannelCloseTimeout { get; set; }
175+
public TimeSpan ChannelCloseTimeout
176+
{
177+
get
178+
{
179+
return _channelCloseTimeout;
180+
}
181+
set
182+
{
183+
value.EnsureValidTimeout(nameof(ChannelCloseTimeout));
184+
185+
_channelCloseTimeout = value;
186+
}
187+
}
161188

162189
/// <summary>
163190
/// Gets or sets the character encoding.

src/Renci.SshNet/ForwardedPort.cs

+2
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ public void Dispose()
102102
/// <param name="timeout">The maximum amount of time to wait for pending requests to finish processing.</param>
103103
protected virtual void StopPort(TimeSpan timeout)
104104
{
105+
timeout.EnsureValidTimeout(nameof(timeout));
106+
105107
RaiseClosing();
106108

107109
var session = Session;

src/Renci.SshNet/ForwardedPortDynamic.cs

+2
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ protected override void StartPort()
101101
/// <param name="timeout">The maximum amount of time to wait for pending requests to finish processing.</param>
102102
protected override void StopPort(TimeSpan timeout)
103103
{
104+
timeout.EnsureValidTimeout(nameof(timeout));
105+
104106
if (!ForwardedPortStatus.ToStopping(ref _status))
105107
{
106108
return;

src/Renci.SshNet/ForwardedPortLocal.cs

+2
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ protected override void StartPort()
138138
/// <param name="timeout">The maximum amount of time to wait for pending requests to finish processing.</param>
139139
protected override void StopPort(TimeSpan timeout)
140140
{
141+
timeout.EnsureValidTimeout(nameof(timeout));
142+
141143
if (!ForwardedPortStatus.ToStopping(ref _status))
142144
{
143145
return;

src/Renci.SshNet/ForwardedPortRemote.cs

+2
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ protected override void StartPort()
188188
/// <param name="timeout">The maximum amount of time to wait for the port to stop.</param>
189189
protected override void StopPort(TimeSpan timeout)
190190
{
191+
timeout.EnsureValidTimeout(nameof(timeout));
192+
191193
if (!ForwardedPortStatus.ToStopping(ref _status))
192194
{
193195
return;

src/Renci.SshNet/NetConfClient.cs

+1-7
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,7 @@ public TimeSpan OperationTimeout
3636
}
3737
set
3838
{
39-
var timeoutInMilliseconds = value.TotalMilliseconds;
40-
if (timeoutInMilliseconds is < -1d or > int.MaxValue)
41-
{
42-
throw new ArgumentOutOfRangeException(nameof(value), "The timeout must represent a value between -1 and Int32.MaxValue, inclusive.");
43-
}
44-
45-
_operationTimeout = (int) timeoutInMilliseconds;
39+
_operationTimeout = value.AsTimeout(nameof(OperationTimeout));
4640
}
4741
}
4842

src/Renci.SshNet/ScpClient.cs

+14-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public partial class ScpClient : BaseClient
3838
private static readonly Regex TimestampRe = new Regex(@"T(?<mtime>\d+) 0 (?<atime>\d+) 0", RegexOptions.Compiled);
3939

4040
private IRemotePathTransformation _remotePathTransformation;
41+
private TimeSpan _operationTimeout;
4142

4243
/// <summary>
4344
/// Gets or sets the operation timeout.
@@ -46,7 +47,19 @@ public partial class ScpClient : BaseClient
4647
/// The timeout to wait until an operation completes. The default value is negative
4748
/// one (-1) milliseconds, which indicates an infinite time-out period.
4849
/// </value>
49-
public TimeSpan OperationTimeout { get; set; }
50+
public TimeSpan OperationTimeout
51+
{
52+
get
53+
{
54+
return _operationTimeout;
55+
}
56+
set
57+
{
58+
value.EnsureValidTimeout(nameof(OperationTimeout));
59+
60+
_operationTimeout = value;
61+
}
62+
}
5063

5164
/// <summary>
5265
/// Gets or sets the size of the buffer.

src/Renci.SshNet/Sftp/SftpFileStream.cs

+14-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public class SftpFileStream : Stream
3535
private bool _canRead;
3636
private bool _canSeek;
3737
private bool _canWrite;
38+
private TimeSpan _timeout;
3839

3940
/// <summary>
4041
/// Gets a value indicating whether the current stream supports reading.
@@ -176,7 +177,19 @@ public virtual byte[] Handle
176177
/// <value>
177178
/// The timeout.
178179
/// </value>
179-
public TimeSpan Timeout { get; set; }
180+
public TimeSpan Timeout
181+
{
182+
get
183+
{
184+
return _timeout;
185+
}
186+
set
187+
{
188+
value.EnsureValidTimeout(nameof(Timeout));
189+
190+
_timeout = value;
191+
}
192+
}
180193

181194
private SftpFileStream(ISftpSession session, string path, FileAccess access, int bufferSize, byte[] handle, long position)
182195
{

src/Renci.SshNet/SftpClient.cs

+1-7
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,7 @@ public TimeSpan OperationTimeout
5959
{
6060
CheckDisposed();
6161

62-
var timeoutInMilliseconds = value.TotalMilliseconds;
63-
if (timeoutInMilliseconds is < -1d or > int.MaxValue)
64-
{
65-
throw new ArgumentOutOfRangeException(nameof(value), "The timeout must represent a value between -1 and Int32.MaxValue, inclusive.");
66-
}
67-
68-
_operationTimeout = (int) timeoutInMilliseconds;
62+
_operationTimeout = value.AsTimeout(nameof(OperationTimeout));
6963
}
7064
}
7165

src/Renci.SshNet/SshCommand.cs

+14-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public class SshCommand : IDisposable
3232
private bool _hasError;
3333
private bool _isDisposed;
3434
private ChannelInputStream _inputStream;
35+
private TimeSpan _commandTimeout;
3536

3637
/// <summary>
3738
/// Gets the command text.
@@ -44,7 +45,19 @@ public class SshCommand : IDisposable
4445
/// <value>
4546
/// The command timeout.
4647
/// </value>
47-
public TimeSpan CommandTimeout { get; set; }
48+
public TimeSpan CommandTimeout
49+
{
50+
get
51+
{
52+
return _commandTimeout;
53+
}
54+
set
55+
{
56+
value.EnsureValidTimeout(nameof(CommandTimeout));
57+
58+
_commandTimeout = value;
59+
}
60+
}
4861

4962
/// <summary>
5063
/// Gets the command exit status.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
using System;
2+
3+
using Microsoft.VisualStudio.TestTools.UnitTesting;
4+
5+
using Renci.SshNet.Common;
6+
using Renci.SshNet.Tests.Common;
7+
8+
namespace Renci.SshNet.Tests.Classes.Common
9+
{
10+
[TestClass]
11+
public class TimeSpanExtensionsTest
12+
{
13+
[TestMethod]
14+
public void AsTimeout_ValidTimeSpan_ReturnsExpectedMilliseconds()
15+
{
16+
var timeSpan = TimeSpan.FromSeconds(10);
17+
18+
var timeout = timeSpan.AsTimeout("TestMethodName");
19+
20+
Assert.AreEqual(10000, timeout);
21+
}
22+
23+
[TestMethod]
24+
[ExpectedException(typeof(ArgumentOutOfRangeException))]
25+
public void AsTimeout_NegativeTimeSpan_ThrowsArgumentOutOfRangeException()
26+
{
27+
var timeSpan = TimeSpan.FromSeconds(-1);
28+
29+
timeSpan.AsTimeout("TestMethodName");
30+
}
31+
32+
[TestMethod]
33+
[ExpectedException(typeof(ArgumentOutOfRangeException))]
34+
public void AsTimeout_TimeSpanExceedingMaxValue_ThrowsArgumentOutOfRangeException()
35+
{
36+
var timeSpan = TimeSpan.FromMilliseconds((double) int.MaxValue + 1);
37+
38+
timeSpan.AsTimeout("TestMethodName");
39+
}
40+
41+
[TestMethod]
42+
public void AsTimeout_ArgumentOutOfRangeException_HasCorrectInformation()
43+
{
44+
45+
try
46+
{
47+
var timeSpan = TimeSpan.FromMilliseconds((double) int.MaxValue + 1);
48+
49+
timeSpan.AsTimeout("TestMethodName");
50+
}
51+
catch (ArgumentOutOfRangeException ex)
52+
{
53+
Assert.IsNull(ex.InnerException);
54+
ArgumentExceptionAssert.MessageEquals("The timeout must represent a value between -1 and Int32.MaxValue milliseconds, inclusive.", ex);
55+
Assert.AreEqual("TestMethodName", ex.ParamName);
56+
}
57+
}
58+
59+
[TestMethod]
60+
public void EnsureValidTimeout_ValidTimeSpan_DoesNotThrow()
61+
{
62+
var timeSpan = TimeSpan.FromSeconds(5);
63+
64+
timeSpan.EnsureValidTimeout("TestMethodName");
65+
}
66+
67+
[TestMethod]
68+
[ExpectedException(typeof(ArgumentOutOfRangeException))]
69+
public void EnsureValidTimeout_NegativeTimeSpan_ThrowsArgumentOutOfRangeException()
70+
{
71+
var timeSpan = TimeSpan.FromSeconds(-1);
72+
73+
timeSpan.EnsureValidTimeout("TestMethodName");
74+
}
75+
76+
[TestMethod]
77+
[ExpectedException(typeof(ArgumentOutOfRangeException))]
78+
public void EnsureValidTimeout_TimeSpanExceedingMaxValue_ThrowsArgumentOutOfRangeException()
79+
{
80+
var timeSpan = TimeSpan.FromMilliseconds((double) int.MaxValue + 1);
81+
82+
timeSpan.EnsureValidTimeout("TestMethodName");
83+
}
84+
85+
[TestMethod]
86+
public void EnsureValidTimeout_ArgumentOutOfRangeException_HasCorrectInformation()
87+
{
88+
89+
try
90+
{
91+
var timeSpan = TimeSpan.FromMilliseconds((double) int.MaxValue + 1);
92+
93+
timeSpan.EnsureValidTimeout("TestMethodName");
94+
}
95+
catch (ArgumentOutOfRangeException ex)
96+
{
97+
Assert.IsNull(ex.InnerException);
98+
ArgumentExceptionAssert.MessageEquals("The timeout must represent a value between -1 and Int32.MaxValue milliseconds, inclusive.", ex);
99+
Assert.AreEqual("TestMethodName", ex.ParamName);
100+
}
101+
}
102+
}
103+
}

0 commit comments

Comments
 (0)