Skip to content

Commit 153b47d

Browse files
authored
Add an OrderedDictionary implementation for algorithm priorities (#1611)
* Add an OrderedDictionary implementation for algorithm priorities During the key exchange, the algorithms to be used are chosen based on the order that the client sends: first algorithm is most desirable. Currently, the algorithm collections in ConnectionInfo are defined as IDictionary<,> and backed by Dictionary<,>, which does not have any guarantees on the order of enumeration (in practice, when only adding and not removing items it does enumerate in the order that items were added as an implementation detail, but it's not great to rely on it). This change adds IOrderedDictionary<,> and uses it in ConnectionInfo. On .NET 9, this is backed by System.Collections.Generic.OrderedDictionary<,> and on lower targets, it uses a relatively simple implementation backed by a List and a Dictionary. * use ThrowIfNegative
1 parent 484afbd commit 153b47d

File tree

7 files changed

+1430
-22
lines changed

7 files changed

+1430
-22
lines changed

src/Renci.SshNet/Common/Extensions.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,5 +358,30 @@ internal static string Join(this IEnumerable<string> values, string separator)
358358
// which is not available on all targets.
359359
return string.Join(separator, values);
360360
}
361+
362+
#if NETFRAMEWORK || NETSTANDARD2_0
363+
internal static bool TryAdd<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key, TValue value)
364+
{
365+
if (!dictionary.ContainsKey(key))
366+
{
367+
dictionary.Add(key, value);
368+
return true;
369+
}
370+
371+
return false;
372+
}
373+
374+
internal static bool Remove<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key, out TValue value)
375+
{
376+
if (dictionary.TryGetValue(key, out value))
377+
{
378+
_ = dictionary.Remove(key);
379+
return true;
380+
}
381+
382+
value = default;
383+
return false;
384+
}
385+
#endif
361386
}
362387
}

src/Renci.SshNet/ConnectionInfo.cs

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -50,42 +50,40 @@ public class ConnectionInfo : IConnectionInfoInternal
5050
/// <summary>
5151
/// Gets supported key exchange algorithms for this connection.
5252
/// </summary>
53-
public IDictionary<string, Func<IKeyExchange>> KeyExchangeAlgorithms { get; private set; }
53+
public IOrderedDictionary<string, Func<IKeyExchange>> KeyExchangeAlgorithms { get; }
5454

5555
/// <summary>
5656
/// Gets supported encryptions for this connection.
5757
/// </summary>
58-
#pragma warning disable CA1859 // Use concrete types when possible for improved performance
59-
public IDictionary<string, CipherInfo> Encryptions { get; private set; }
60-
#pragma warning restore CA1859 // Use concrete types when possible for improved performance
58+
public IOrderedDictionary<string, CipherInfo> Encryptions { get; }
6159

6260
/// <summary>
6361
/// Gets supported hash algorithms for this connection.
6462
/// </summary>
65-
public IDictionary<string, HashInfo> HmacAlgorithms { get; private set; }
63+
public IOrderedDictionary<string, HashInfo> HmacAlgorithms { get; }
6664

6765
/// <summary>
6866
/// Gets supported host key algorithms for this connection.
6967
/// </summary>
70-
public IDictionary<string, Func<byte[], KeyHostAlgorithm>> HostKeyAlgorithms { get; private set; }
68+
public IOrderedDictionary<string, Func<byte[], KeyHostAlgorithm>> HostKeyAlgorithms { get; }
7169

7270
/// <summary>
7371
/// Gets supported authentication methods for this connection.
7472
/// </summary>
75-
public IList<AuthenticationMethod> AuthenticationMethods { get; private set; }
73+
public IList<AuthenticationMethod> AuthenticationMethods { get; }
7674

7775
/// <summary>
7876
/// Gets supported compression algorithms for this connection.
7977
/// </summary>
80-
public IDictionary<string, Func<Compressor>> CompressionAlgorithms { get; private set; }
78+
public IOrderedDictionary<string, Func<Compressor>> CompressionAlgorithms { get; }
8179

8280
/// <summary>
8381
/// Gets the supported channel requests for this connection.
8482
/// </summary>
8583
/// <value>
8684
/// The supported channel requests for this connection.
8785
/// </value>
88-
public IDictionary<string, RequestInfo> ChannelRequests { get; private set; }
86+
public IDictionary<string, RequestInfo> ChannelRequests { get; }
8987

9088
/// <summary>
9189
/// Gets a value indicating whether connection is authenticated.
@@ -101,48 +99,48 @@ public class ConnectionInfo : IConnectionInfoInternal
10199
/// <value>
102100
/// The connection host.
103101
/// </value>
104-
public string Host { get; private set; }
102+
public string Host { get; }
105103

106104
/// <summary>
107105
/// Gets connection port.
108106
/// </summary>
109107
/// <value>
110108
/// The connection port. The default value is 22.
111109
/// </value>
112-
public int Port { get; private set; }
110+
public int Port { get; }
113111

114112
/// <summary>
115113
/// Gets connection username.
116114
/// </summary>
117-
public string Username { get; private set; }
115+
public string Username { get; }
118116

119117
/// <summary>
120118
/// Gets proxy type.
121119
/// </summary>
122120
/// <value>
123121
/// The type of the proxy.
124122
/// </value>
125-
public ProxyTypes ProxyType { get; private set; }
123+
public ProxyTypes ProxyType { get; }
126124

127125
/// <summary>
128126
/// Gets proxy connection host.
129127
/// </summary>
130-
public string ProxyHost { get; private set; }
128+
public string ProxyHost { get; }
131129

132130
/// <summary>
133131
/// Gets proxy connection port.
134132
/// </summary>
135-
public int ProxyPort { get; private set; }
133+
public int ProxyPort { get; }
136134

137135
/// <summary>
138136
/// Gets proxy connection username.
139137
/// </summary>
140-
public string ProxyUsername { get; private set; }
138+
public string ProxyUsername { get; }
141139

142140
/// <summary>
143141
/// Gets proxy connection password.
144142
/// </summary>
145-
public string ProxyPassword { get; private set; }
143+
public string ProxyPassword { get; }
146144

147145
/// <summary>
148146
/// Gets or sets connection timeout.
@@ -347,7 +345,7 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy
347345
MaxSessions = 10;
348346
Encoding = Encoding.UTF8;
349347

350-
KeyExchangeAlgorithms = new Dictionary<string, Func<IKeyExchange>>
348+
KeyExchangeAlgorithms = new OrderedDictionary<string, Func<IKeyExchange>>
351349
{
352350
{ "mlkem768x25519-sha256", () => new KeyExchangeMLKem768X25519Sha256() },
353351
{ "sntrup761x25519-sha512", () => new KeyExchangeSNtruP761X25519Sha512() },
@@ -365,7 +363,7 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy
365363
{ "diffie-hellman-group1-sha1", () => new KeyExchangeDiffieHellmanGroup1Sha1() },
366364
};
367365

368-
Encryptions = new Dictionary<string, CipherInfo>
366+
Encryptions = new OrderedDictionary<string, CipherInfo>
369367
{
370368
{ "aes128-ctr", new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)) },
371369
{ "aes192-ctr", new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)) },
@@ -379,7 +377,7 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy
379377
{ "3des-cbc", new CipherInfo(192, (key, iv) => new TripleDesCipher(key, iv, CipherMode.CBC, pkcs7Padding: false)) },
380378
};
381379

382-
HmacAlgorithms = new Dictionary<string, HashInfo>
380+
HmacAlgorithms = new OrderedDictionary<string, HashInfo>
383381
{
384382
/* Encrypt-and-MAC (encrypt-and-authenticate) variants */
385383
{ "hmac-sha2-256", new HashInfo(32*8, key => new HMACSHA256(key)) },
@@ -392,7 +390,7 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy
392390
};
393391

394392
#pragma warning disable SA1107 // Code should not contain multiple statements on one line
395-
var hostAlgs = new Dictionary<string, Func<byte[], KeyHostAlgorithm>>();
393+
var hostAlgs = new OrderedDictionary<string, Func<byte[], KeyHostAlgorithm>>();
396394
hostAlgs.Add("[email protected]", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("[email protected]", cert, hostAlgs); });
397395
hostAlgs.Add("[email protected]", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("[email protected]", cert, hostAlgs); });
398396
hostAlgs.Add("[email protected]", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("[email protected]", cert, hostAlgs); });
@@ -411,7 +409,7 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy
411409
#pragma warning restore SA1107 // Code should not contain multiple statements on one line
412410
HostKeyAlgorithms = hostAlgs;
413411

414-
CompressionAlgorithms = new Dictionary<string, Func<Compressor>>
412+
CompressionAlgorithms = new OrderedDictionary<string, Func<Compressor>>
415413
{
416414
{ "none", null },
417415
{ "[email protected]", () => new ZlibOpenSsh() },
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
4+
5+
namespace Renci.SshNet
6+
{
7+
/// <summary>
8+
/// Represents a collection of key/value pairs that are accessible by the key or index.
9+
/// </summary>
10+
/// <typeparam name="TKey">The type of the keys in the dictionary.</typeparam>
11+
/// <typeparam name="TValue">The type of the values in the dictionary.</typeparam>
12+
public interface IOrderedDictionary<TKey, TValue> :
13+
IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue>
14+
where TKey : notnull
15+
{
16+
// Some members are redefined with 'new' to resolve ambiguities.
17+
18+
/// <summary>Gets or sets the value associated with the specified key.</summary>
19+
/// <param name="key">The key of the value to get or set.</param>
20+
/// <returns>The value associated with the specified key. If the specified key is not found, a get operation throws a <see cref="KeyNotFoundException"/>, and a set operation creates a new element with the specified key.</returns>
21+
/// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception>
22+
/// <exception cref="KeyNotFoundException">The property is retrieved and <paramref name="key"/> does not exist in the collection.</exception>
23+
/// <remarks>Setting the value of an existing key does not impact its order in the collection.</remarks>
24+
new TValue this[TKey key] { get; set; }
25+
26+
/// <summary>Gets a collection containing the keys in the <see cref="IOrderedDictionary{TKey, TValue}"/>.</summary>
27+
new ICollection<TKey> Keys { get; }
28+
29+
/// <summary>Gets a collection containing the values in the <see cref="IOrderedDictionary{TKey, TValue}"/>.</summary>
30+
new ICollection<TValue> Values { get; }
31+
32+
/// <summary>Gets the number of key/value pairs contained in the <see cref="IOrderedDictionary{TKey, TValue}"/>.</summary>
33+
new int Count { get; }
34+
35+
/// <summary>Determines whether the <see cref="IOrderedDictionary{TKey, TValue}"/> contains the specified key.</summary>
36+
/// <param name="key">The key to locate in the <see cref="IOrderedDictionary{TKey, TValue}"/>.</param>
37+
/// <returns><see langword="true"/> if the <see cref="IOrderedDictionary{TKey, TValue}"/> contains an element with the specified key; otherwise, <see langword="false"/>.</returns>
38+
/// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception>
39+
new bool ContainsKey(TKey key);
40+
41+
/// <summary>Determines whether the <see cref="IOrderedDictionary{TKey, TValue}"/> contains a specific value.</summary>
42+
/// <param name="value">The value to locate in the <see cref="IOrderedDictionary{TKey, TValue}"/>. The value can be null for reference types.</param>
43+
/// <returns><see langword="true"/> if the <see cref="IOrderedDictionary{TKey, TValue}"/> contains an element with the specified value; otherwise, <see langword="false"/>.</returns>
44+
bool ContainsValue(TValue value);
45+
46+
/// <summary>Gets the key/value pair at the specified index.</summary>
47+
/// <param name="index">The zero-based index of the pair to get.</param>
48+
/// <returns>The element at the specified index.</returns>
49+
/// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is less than 0 or greater than or equal to <see cref="Count"/>.</exception>
50+
KeyValuePair<TKey, TValue> GetAt(int index);
51+
52+
/// <summary>Determines the index of a specific key in the <see cref="IOrderedDictionary{TKey, TValue}"/>.</summary>
53+
/// <param name="key">The key to locate.</param>
54+
/// <returns>The index of <paramref name="key"/> if found; otherwise, -1.</returns>
55+
/// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception>
56+
int IndexOf(TKey key);
57+
58+
/// <summary>Inserts an item into the collection at the specified index.</summary>
59+
/// <param name="index">The zero-based index at which item should be inserted.</param>
60+
/// <param name="key">The key to insert.</param>
61+
/// <param name="value">The value to insert.</param>
62+
/// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception>
63+
/// <exception cref="ArgumentException">An element with the same key already exists in the <see cref="IOrderedDictionary{TKey, TValue}"/>.</exception>
64+
/// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is less than 0 or greater than <see cref="Count"/>.</exception>
65+
void Insert(int index, TKey key, TValue value);
66+
67+
/// <summary>Removes the value with the specified key from the <see cref="IOrderedDictionary{TKey, TValue}"/> and copies the element to the value parameter.</summary>
68+
/// <param name="key">The key of the element to remove.</param>
69+
/// <param name="value">The removed element.</param>
70+
/// <returns><see langword="true"/> if the element is successfully found and removed; otherwise, <see langword="false"/>.</returns>
71+
/// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception>
72+
bool Remove(TKey key, [MaybeNullWhen(false)] out TValue value);
73+
74+
/// <summary>Removes the key/value pair at the specified index.</summary>
75+
/// <param name="index">The zero-based index of the item to remove.</param>
76+
/// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is less than 0 or greater than or equal to <see cref="Count"/>.</exception>
77+
void RemoveAt(int index);
78+
79+
/// <summary>Sets the key/value pair at the specified index.</summary>
80+
/// <param name="index">The zero-based index at which to set the key/value pair.</param>
81+
/// <param name="key">The key to store at the specified index.</param>
82+
/// <param name="value">The value to store at the specified index.</param>
83+
/// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception>
84+
/// <exception cref="ArgumentException">An element with the same key already exists at an index different to <paramref name="index"/>.</exception>
85+
/// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is less than 0 or greater than or equal to <see cref="Count"/>.</exception>
86+
void SetAt(int index, TKey key, TValue value);
87+
88+
/// <summary>Sets the value for the key at the specified index.</summary>
89+
/// <param name="index">The zero-based index at which to set the key/value pair.</param>
90+
/// <param name="value">The value to store at the specified index.</param>
91+
/// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is less than 0 or greater than or equal to <see cref="Count"/>.</exception>
92+
void SetAt(int index, TValue value);
93+
94+
/// <summary>
95+
/// Moves an existing key/value pair to the specified index in the collection.
96+
/// </summary>
97+
/// <param name="index">The current zero-based index of the key/value pair to move.</param>
98+
/// <param name="newIndex">The zero-based index at which to set the key/value pair.</param>
99+
/// <exception cref="ArgumentOutOfRangeException">
100+
/// <paramref name="index"/> or <paramref name="newIndex"/> are less than 0 or greater than or equal to <see cref="Count"/>.
101+
/// </exception>
102+
void SetPosition(int index, int newIndex);
103+
104+
/// <summary>
105+
/// Moves an existing key/value pair to the specified index in the collection.
106+
/// </summary>
107+
/// <param name="key">The key to move.</param>
108+
/// <param name="newIndex">The zero-based index at which to set the key/value pair.</param>
109+
/// <exception cref="KeyNotFoundException">The specified key does not exist in the collection.</exception>
110+
/// <exception cref="ArgumentOutOfRangeException"><paramref name="newIndex"/> is less than 0 or greater than or equal to <see cref="Count"/>.</exception>
111+
void SetPosition(TKey key, int newIndex);
112+
113+
/// <summary>Adds the specified key and value to the dictionary if the key doesn't already exist.</summary>
114+
/// <param name="key">The key of the element to add.</param>
115+
/// <param name="value">The value of the element to add. The value can be <see langword="null"/> for reference types.</param>
116+
/// <returns><see langword="true"/> if the key didn't exist and the key and value were added to the dictionary; otherwise, <see langword="false"/>.</returns>
117+
/// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception>
118+
bool TryAdd(TKey key, TValue value);
119+
120+
/// <summary>Adds the specified key and value to the dictionary if the key doesn't already exist.</summary>
121+
/// <param name="key">The key of the element to add.</param>
122+
/// <param name="value">The value of the element to add. The value can be <see langword="null"/> for reference types.</param>
123+
/// <param name="index">The index of the added or existing <paramref name="key"/>. This is always a valid index into the dictionary.</param>
124+
/// <returns><see langword="true"/> if the key didn't exist and the key and value were added to the dictionary; otherwise, <see langword="false"/>.</returns>
125+
/// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception>
126+
bool TryAdd(TKey key, TValue value, out int index);
127+
128+
/// <summary>Gets the value associated with the specified key.</summary>
129+
/// <param name="key">The key of the value to get.</param>
130+
/// <param name="value">
131+
/// When this method returns, contains the value associated with the specified key, if the key is found;
132+
/// otherwise, the default value for the type of the value parameter.
133+
/// </param>
134+
/// <returns><see langword="true"/> if the <see cref="IOrderedDictionary{TKey, TValue}"/> contains an element with the specified key; otherwise, <see langword="false"/>.</returns>
135+
/// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception>
136+
new bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value);
137+
138+
/// <summary>Gets the value associated with the specified key.</summary>
139+
/// <param name="key">The key of the value to get.</param>
140+
/// <param name="value">
141+
/// When this method returns, contains the value associated with the specified key, if the key is found;
142+
/// otherwise, the default value for the type of the value parameter.
143+
/// </param>
144+
/// <param name="index">The index of <paramref name="key"/> if found; otherwise, -1.</param>
145+
/// <returns><see langword="true"/> if the <see cref="IOrderedDictionary{TKey, TValue}"/> contains an element with the specified key; otherwise, <see langword="false"/>.</returns>
146+
/// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception>
147+
bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value, out int index);
148+
}
149+
}

0 commit comments

Comments
 (0)