Skip to content

Commit cf021ac

Browse files
authored
Enhance STUNUri and IPSocket with modern C# practices (#1409)
Updated SIPSorcery.csproj to include new package references for `System.Memory` and `Microsoft.Bcl.HashCode` for .NET Framework 4.6.2 and .NET Standard 2.0. Refactored `STUNUri` class to implement `IEquatable<STUNUri>`, changed public fields to read-only properties, and overloaded the constructor for better initialization options. Improved `ParseSTUNUri` method with a robust `TryParse` implementation and optimized parsing logic. Simplified `Equals` and `GetHashCode` methods for better performance. Enhanced `TryParseIPEndPoint` in `IPSocket.cs` to accept `ReadOnlySpan<char>` for improved efficiency and memory usage. Overall, these changes promote immutability, performance, and maintainability in line with modern C# standards.
1 parent 6934790 commit cf021ac

File tree

3 files changed

+131
-88
lines changed

3 files changed

+131
-88
lines changed

src/SIPSorcery.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929
<!-- The packages below are transitive references included to overcome vulnerabilities in a top level package. -->
3030
<PackageReference Include="System.Net.Security" Version="4.3.2" /> <!-- Vuln version referenced by ystem.Net.WebSockets.Client. -->
3131
</ItemGroup>
32+
33+
<ItemGroup Condition="'$(TargetFramework)' == 'net462' OR '$(TargetFramework)' == 'netstandard2.0'">
34+
<PackageReference Include="System.Memory" Version="4.6.3" />
35+
<PackageReference Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
36+
</ItemGroup>
3237

3338
<PropertyGroup>
3439
<TargetFrameworks>netstandard2.0;netstandard2.1;netcoreapp3.1;net462;net5.0;net6.0;net8.0</TargetFrameworks>

src/net/STUN/STUNUri.cs

Lines changed: 101 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
//-----------------------------------------------------------------------------
1818

1919
using System;
20+
using System.ComponentModel;
2021
using System.Net.Sockets;
2122
using SIPSorcery.Sys;
2223

@@ -79,29 +80,29 @@ public enum STUNProtocolsEnum
7980
dtls = 4,
8081
}
8182

82-
public class STUNUri
83+
public sealed class STUNUri : IEquatable<STUNUri>
8384
{
8485
public const string SCHEME_TRANSPORT_TCP = "transport=tcp";
8586
public const string SCHEME_TRANSPORT_TLS = "transport=tls";
8687

87-
public static readonly string[] SCHEME_TRANSPORT_SEPARATOR = { "?transport=" };
88+
public static readonly string SCHEME_TRANSPORT_SEPARATOR = "?transport=";
8889
public const char SCHEME_ADDR_SEPARATOR = ':';
8990
public const int SCHEME_MAX_LENGTH = 5;
9091

9192
public const STUNSchemesEnum DefaultSTUNScheme = STUNSchemesEnum.stun;
9293

93-
public STUNProtocolsEnum Transport = STUNProtocolsEnum.udp;
94-
public STUNSchemesEnum Scheme = DefaultSTUNScheme;
94+
public STUNProtocolsEnum Transport { get; } = STUNProtocolsEnum.udp;
95+
public STUNSchemesEnum Scheme { get; } = DefaultSTUNScheme;
9596

96-
public string Host;
97-
public int Port;
97+
public string Host { get; }
98+
public int Port { get; }
9899

99100
/// <summary>
100101
/// If the port is specified in a URI it affects the way a DNS lookup occurs.
101102
/// An explicit port means to lookup the A or AAAA record directly without
102103
/// checking for SRV records.
103104
/// </summary>
104-
public bool ExplicitPort;
105+
public bool ExplicitPort { get; }
105106

106107
/// <summary>
107108
/// The network protocol for this URI type.
@@ -124,107 +125,126 @@ public ProtocolType Protocol
124125
private STUNUri()
125126
{ }
126127

127-
public STUNUri(STUNSchemesEnum scheme, string host, int port = STUNConstants.DEFAULT_STUN_PORT)
128+
[EditorBrowsable(EditorBrowsableState.Advanced)]
129+
public STUNUri(STUNSchemesEnum scheme, string host, int port)
128130
{
129131
Scheme = scheme;
130132
Host = host;
131133
Port = port;
132134
}
133135

134-
public static STUNUri ParseSTUNUri(string uri)
136+
public STUNUri(STUNSchemesEnum scheme, string host, int port = STUNConstants.DEFAULT_STUN_PORT, STUNProtocolsEnum transport = STUNProtocolsEnum.udp, bool explicitPort = false)
135137
{
136-
STUNUri stunUri = new STUNUri();
138+
Scheme = scheme;
139+
Host = host;
140+
Port = port;
141+
Transport = transport;
142+
ExplicitPort = explicitPort;
143+
}
137144

138-
if (String.IsNullOrEmpty(uri))
145+
public static STUNUri ParseSTUNUri(string uriStr)
146+
{
147+
if (!TryParse(uriStr, out var uri))
139148
{
140149
throw new ApplicationException("A STUN URI cannot be parsed from an empty string.");
141150
}
142-
else
151+
152+
return uri;
153+
}
154+
155+
public static bool TryParse(string uriStr, out STUNUri uri)
156+
{
157+
uri = null;
158+
159+
if (string.IsNullOrEmpty(uriStr))
143160
{
144-
//Split uri to include support to transport detection
145-
var splitUri = uri.Split(SCHEME_TRANSPORT_SEPARATOR, StringSplitOptions.RemoveEmptyEntries);
146-
if(splitUri.Length > 1)
161+
return false;
162+
}
163+
164+
ReadOnlySpan<char> uriSpan = uriStr.AsSpan();
165+
STUNProtocolsEnum transport = STUNProtocolsEnum.udp;
166+
167+
// Handle transport protocol
168+
int transportIndex = uriSpan.IndexOf('?');
169+
if (transportIndex >= 0 && uriSpan.Slice(transportIndex, SCHEME_TRANSPORT_SEPARATOR.Length).SequenceEqual(SCHEME_TRANSPORT_SEPARATOR.AsSpan()))
170+
{
171+
var protocolSpan = uriSpan.Slice(transportIndex + SCHEME_TRANSPORT_SEPARATOR.Length).Trim();
172+
#if NET6_0_OR_GREATER
173+
if (!protocolSpan.IsEmpty && !Enum.TryParse(protocolSpan, true, out transport))
174+
#else
175+
if (!protocolSpan.IsEmpty && !Enum.TryParse(protocolSpan.ToString(), true, out transport))
176+
#endif
147177
{
148-
uri = splitUri[0];
149-
var protocolStr = splitUri[1].Trim();
150-
if (string.IsNullOrEmpty(protocolStr) || !Enum.TryParse<STUNProtocolsEnum>(protocolStr, true, out stunUri.Transport))
151-
{
152-
stunUri.Transport = STUNProtocolsEnum.udp;
153-
}
178+
transport = STUNProtocolsEnum.udp;
154179
}
180+
uriSpan = uriSpan.Slice(0, transportIndex);
181+
}
155182

156-
uri = uri.Trim();
183+
uriSpan = uriSpan.Trim();
184+
var scheme = DefaultSTUNScheme;
157185

158-
// If the scheme is included it needs to be within the first 5 characters.
159-
if (uri.Length > SCHEME_MAX_LENGTH + 2)
160-
{
161-
string schemeStr = uri.Substring(0, SCHEME_MAX_LENGTH + 1);
162-
int colonPosn = schemeStr.IndexOf(SCHEME_ADDR_SEPARATOR);
186+
// Handle scheme parsing
187+
if (uriSpan.Length > SCHEME_MAX_LENGTH + 2)
188+
{
189+
ReadOnlySpan<char> schemeSpan = uriSpan.Slice(0, SCHEME_MAX_LENGTH + 1);
190+
int colonPosn = schemeSpan.IndexOf(SCHEME_ADDR_SEPARATOR);
163191

164-
if (colonPosn == -1)
165-
{
166-
// No scheme has been specified, use default.
167-
stunUri.Scheme = DefaultSTUNScheme;
168-
}
169-
else
192+
if (colonPosn >= 0)
193+
{
194+
#if NET6_0_OR_GREATER
195+
if (!Enum.TryParse(schemeSpan.Slice(0, colonPosn), true, out scheme))
196+
#else
197+
if (!Enum.TryParse(schemeSpan.Slice(0, colonPosn).ToString(), true, out scheme))
198+
#endif
170199
{
171-
if (!Enum.TryParse<STUNSchemesEnum>(schemeStr.Substring(0, colonPosn), true, out stunUri.Scheme))
172-
{
173-
stunUri.Scheme = DefaultSTUNScheme;
174-
}
175-
176-
uri = uri.Substring(colonPosn + 1);
200+
scheme = DefaultSTUNScheme;
177201
}
202+
uriSpan = uriSpan.Slice(colonPosn + 1);
178203
}
204+
}
179205

180-
if (uri.IndexOf(':') != -1)
181-
{
182-
stunUri.ExplicitPort = true;
206+
var explicitPort = false;
207+
int port;
208+
string host;
209+
210+
int lastColonPos = uriSpan.LastIndexOf(':');
211+
if (lastColonPos != -1)
212+
{
213+
explicitPort = true;
183214

184-
if (IPSocket.TryParseIPEndPoint(uri, out var ipEndPoint))
215+
if (IPSocket.TryParseIPEndPoint(uriSpan, out var ipEndPoint))
216+
{
217+
if (ipEndPoint.AddressFamily == AddressFamily.InterNetworkV6)
185218
{
186-
if (ipEndPoint.AddressFamily == AddressFamily.InterNetworkV6)
187-
{
188-
stunUri.Host = $"[{ipEndPoint.Address}]";
189-
}
190-
else
191-
{
192-
stunUri.Host = ipEndPoint.Address.ToString();
193-
}
194-
195-
stunUri.Port = ipEndPoint.Port;
219+
host = $"[{ipEndPoint.Address}]";
196220
}
197221
else
198222
{
199-
stunUri.Host = uri.Substring(0, uri.LastIndexOf(':'));
200-
if (!Int32.TryParse(uri.Substring(uri.LastIndexOf(':') + 1), out stunUri.Port))
201-
{
202-
stunUri.Port = STUNConstants.GetPortForScheme(stunUri.Scheme);
203-
}
223+
host = ipEndPoint.Address.ToString();
204224
}
225+
port = ipEndPoint.Port;
205226
}
206227
else
207228
{
208-
stunUri.Host = uri?.Trim();
209-
stunUri.Port = STUNConstants.GetPortForScheme(stunUri.Scheme);
229+
host = uriSpan.Slice(0, lastColonPos).ToString();
230+
#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER
231+
if (!int.TryParse(uriSpan.Slice(lastColonPos + 1), out port))
232+
#else
233+
if (!int.TryParse(uriSpan.Slice(lastColonPos + 1).ToString(), out port))
234+
#endif
235+
{
236+
port = STUNConstants.GetPortForScheme(scheme);
237+
}
210238
}
211239
}
212-
213-
return stunUri;
214-
}
215-
216-
public static bool TryParse(string uriStr, out STUNUri uri)
217-
{
218-
try
219-
{
220-
uri = ParseSTUNUri(uriStr);
221-
return (uri != null);
222-
}
223-
catch
240+
else
224241
{
225-
uri = null;
226-
return false;
242+
host = uriSpan.ToString();
243+
port = STUNConstants.GetPortForScheme(scheme);
227244
}
245+
246+
uri = new STUNUri(scheme, host, port: port, transport: transport, explicitPort: explicitPort);
247+
return true;
228248
}
229249

230250
public override string ToString()
@@ -261,9 +281,14 @@ public static bool AreEqual(STUNUri uri1, STUNUri uri2)
261281
return uri1 == uri2;
262282
}
263283

284+
public bool Equals(STUNUri other)
285+
{
286+
return (this == other);
287+
}
288+
264289
public override bool Equals(object obj)
265290
{
266-
return AreEqual(this, (STUNUri)obj);
291+
return Equals(this, (STUNUri)obj);
267292
}
268293

269294
public static bool operator ==(STUNUri uri1, STUNUri uri2)
@@ -307,11 +332,7 @@ public override bool Equals(object obj)
307332

308333
public override int GetHashCode()
309334
{
310-
return Scheme.GetHashCode()
311-
+ Transport.GetHashCode()
312-
+ ((Host != null) ? Host.GetHashCode() : 0)
313-
+ Port
314-
+ ((ExplicitPort) ? 1 : 0);
335+
return HashCode.Combine(Scheme, Transport, Host, Port, ExplicitPort);
315336
}
316337
}
317338
}

src/sys/Net/IPSocket.cs

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//-----------------------------------------------------------------------------
1+
//-----------------------------------------------------------------------------
22
// Filename: IPSocket.cs
33
//
44
// Description: Helper functions for socket strings and IP end points.
@@ -57,36 +57,53 @@ public static string GetSocketString(IPEndPoint endPoint)
5757
/// <param name="result">If the parse is successful this output parameter will contain the IPv4 or IPv6 end point.</param>
5858
/// <returns>Returns true if the string could be successfully parsed as an IPv4 or IPv6 end point. False if not.</returns>
5959
public static bool TryParseIPEndPoint(string s, out IPEndPoint result)
60+
=> TryParseIPEndPoint(s.AsSpan(), out result);
61+
62+
/// <summary>
63+
/// This code is based on the IPEndPoint.TryParse method in the dotnet source code at
64+
/// https://github.com/dotnet/corefx/blob/master/src/System.Net.Primitives/src/System/Net/IPEndPoint.cs.
65+
/// If/when that feature makes it into .NET Standard this method can be replaced.
66+
/// </summary>
67+
/// <param name="s">The end point string to parse.</param>
68+
/// <param name="result">If the parse is successful this output parameter will contain the IPv4 or IPv6 end point.</param>
69+
/// <returns>Returns true if the string could be successfully parsed as an IPv4 or IPv6 end point. False if not.</returns>
70+
public static bool TryParseIPEndPoint(ReadOnlySpan<char> s, out IPEndPoint result)
6071
{
61-
int addressLength = s.Length; // If there's no port then send the entire string to the address parser
72+
result = null;
73+
int addressLength = s.Length;
6274
int lastColonPos = s.LastIndexOf(':');
6375

64-
// Look to see if this is an IPv6 address with a port.
6576
if (lastColonPos > 0)
6677
{
6778
if (s[lastColonPos - 1] == ']')
6879
{
6980
addressLength = lastColonPos;
7081
}
71-
// Look to see if this is IPv4 with a port (IPv6 will have another colon)
72-
else if (s.Substring(0, lastColonPos).LastIndexOf(':') == -1)
82+
else if (s.Slice(0, lastColonPos).LastIndexOf(':') == -1)
7383
{
7484
addressLength = lastColonPos;
7585
}
7686
}
7787

78-
if (IPAddress.TryParse(s.Substring(0, addressLength), out IPAddress address))
88+
#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER
89+
if (IPAddress.TryParse(s.Slice(0, addressLength), out IPAddress address))
90+
#else
91+
if (IPAddress.TryParse(s.Slice(0, addressLength).ToString(), out IPAddress address))
92+
#endif
7993
{
8094
uint port = 0;
8195
if (addressLength == s.Length ||
82-
(uint.TryParse(s.Substring(addressLength + 1), NumberStyles.None, CultureInfo.InvariantCulture, out port) && port <= MaxPort))
96+
#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER
97+
(uint.TryParse(s.Slice(addressLength + 1), NumberStyles.None, CultureInfo.InvariantCulture, out port) && port <= MaxPort))
98+
#else
99+
(uint.TryParse(s.Slice(addressLength + 1).ToString(), NumberStyles.None, CultureInfo.InvariantCulture, out port) && port <= MaxPort))
100+
#endif
83101
{
84102
result = new IPEndPoint(address, (int)port);
85103
return true;
86104
}
87105
}
88106

89-
result = null;
90107
return false;
91108
}
92109

0 commit comments

Comments
 (0)